From Theory to iPhone, Pt. 3: A Diversion Into Ruby

As far as I know, you cannot program the iPhone with Ruby. However, you can program OS X Cocoa applications with Ruby and, as a learning experience, it's very helpful.

I am coming to believe that the biggest barrier to learning how to program Cocoa are tutorials that emphasize the toolset rather than providing any context. This is a decades-old complaint of mine with programming tutorials, but that's a subject of another post.

In the case of my Ruby code, here is the object structure:

ruby_mvc

Which is about as classic an MVC triad as you can hope for, the only variation from the classic structure being the use of the singleton NSNotificationCenter rather than a strict OO Observer pattern.

Initialization also follows the classic sequence: the "main()" in the case of a Cocoa app is this Ruby code, executed outside of a class definition:

::: {#codeSnippetWrapper style="line-height: 12pt; background-color: #f4f4f4; margin: 20px 0px 10px; width: 97.5%; font-family: 'Courier New', courier, monospace; max-height: 200px; font-size: 8pt; overflow: auto; cursor: text; border: silver 1px solid; padding: 4px;"} ``` {#codeSnippet style="line-height: 12pt; background-color: #f4f4f4; margin: 0em; width: 100%; font-family: 'Courier New', courier, monospace; color: black; font-size: 8pt; overflow: visible; border-style: none; padding: 0px;"}

set up the application delegate

delegate = ApplicationDelegate.alloc.init() OSX::NSApplication.sharedApplication.setDelegate(delegate)

This will set in motion the initialization, which will eventually call back to the function **applicationDidFinishLaunching(sender)** in the just-created instance of the **ApplicationDelegate** class:

::: {#codeSnippetWrapper style="line-height: 12pt; background-color: #f4f4f4; margin: 20px 0px 10px; width: 97.5%; font-family: 'Courier New', courier, monospace; max-height: 200px; font-size: 8pt; overflow: auto; cursor: text; border: silver 1px solid; padding: 4px;"}
``` {#codeSnippet style="line-height: 12pt; background-color: #f4f4f4; margin: 0em; width: 100%; font-family: 'Courier New', courier, monospace; color: black; font-size: 8pt; overflow: visible; border-style: none; padding: 0px;"}
require 'osx/cocoa'
require 'MyModel'
require 'MyView'
require 'MyController'

class ApplicationDelegate < OSX::NSObject
  def applicationDidFinishLaunching(sender)
    model = MyModel.alloc().init_cocoa()

    #View will subscribe to model notifications. View creates Controller
    view = MyView.alloc().initWithFrame_model([30,20,600,300], model)

    #Do something that will propagate through connections
    model.text = "Hello, MVC!"
  end
end

:::

The applicationDidFinishLaunching(sender) method follows the classic MVC initialization sequence:

  1. Create the Model
  2. Create the View
  3. Pass the Model to the View
    1. View creates its own Controller
    2. View passes Model to Controller

I then manipulate the Model (by setting its text) in order to show how things are wired up.

But before we review that, let's take a look at the View initialization function initWithFrame_model(frame, model)

::: {#codeSnippetWrapper style="line-height: 12pt; background-color: #f4f4f4; margin: 20px 0px 10px; width: 97.5%; font-family: 'Courier New', courier, monospace; max-height: 200px; font-size: 8pt; overflow: auto; cursor: text; border: silver 1px solid; padding: 4px;"} ``` {#codeSnippet style="line-height: 12pt; background-color: #f4f4f4; margin: 0em; width: 100%; font-family: 'Courier New', courier, monospace; color: black; font-size: 8pt; overflow: visible; border-style: none; padding: 0px;"}

This is inside class MyView

def initWithFrame_model(frame, model) #Create a UI styleMask = OSX::NSTitledWindowMask + OSX::NSClosableWindowMask + OSX::NSMiniaturizableWindowMask + OSX::NSResizableWindowMask @window = OSX::NSWindow.alloc.initWithContentRect_styleMask_backing_defer(frame, styleMask, OSX::NSBackingStoreBuffered, false) @textview = OSX::NSTextView.alloc.initWithFrame(frame) @window.setContentView(@textview) @window.setTitle("Ruby Cocoa MVC") @window.center() @window.makeKeyAndOrderFront(self)

#Make controller. Note this must follow initialization of "controlled things" (e.g., @window)
controller = MyController.alloc.initWithView_model(self, model)

#Associate with model. Listen to model updates via NSNotificationCenter
@model = model
@notification_center = OSX::NSNotificationCenter.defaultCenter()
@notification_center.addObserver_selector_name_object_(self, :on_model_updated, 'MyModelUpdatedNotification', @model)

return self

end

First, we setup the View itself using all those Cocoa calls. Then we instantiate the Controller, passing the **model** along (we'll return to **MyController.initWithView\_model(self, model)** shortly). The last step of MyView initialization is registering a callback for when the **model** posts notifications. In this case, the parameters to **addObserver\_selector\_name\_object\_()** mean "When the \@model sends a notification called 'MyModelUpdatedNotification' call the function self.on\_model\_updated()":

::: {#codeSnippetWrapper style="line-height: 12pt; background-color: #f4f4f4; margin: 20px 0px 10px; width: 97.5%; font-family: 'Courier New', courier, monospace; max-height: 200px; font-size: 8pt; overflow: auto; cursor: text; border: silver 1px solid; padding: 4px;"}
``` {#codeSnippet style="line-height: 12pt; background-color: #f4f4f4; margin: 0em; width: 100%; font-family: 'Courier New', courier, monospace; color: black; font-size: 8pt; overflow: visible; border-style: none; padding: 0px;"}
#This is inside class MyView
def on_model_updated(notification)
    puts "MyView.on_model_updated() called"
    @textview.insertText_(@model.text)
end

:::

Very straightforward: this the callback made after the model has been updated. In this case, the View reflects the model's text attribute. It's a very simple Model and a very simple View!

::: {#codeSnippetWrapper style="line-height: 12pt; background-color: #f4f4f4; margin: 20px 0px 10px; width: 97.5%; font-family: 'Courier New', courier, monospace; max-height: 200px; font-size: 8pt; overflow: auto; cursor: text; border: silver 1px solid; padding: 4px;"} ``` {#codeSnippet style="line-height: 12pt; background-color: #f4f4f4; margin: 0em; width: 100%; font-family: 'Courier New', courier, monospace; color: black; font-size: 8pt; overflow: visible; border-style: none; padding: 0px;"} class MyModel < OSX::NSObject def init_cocoa @text = "initial text" return self end

def text=(t)
    puts "Setting text: #{t}"
    @text = t
    #Model notifies observers
    notification_center = OSX::NSNotificationCenter.defaultCenter()
    notification_center.postNotificationName_object_("MyModelUpdatedNotification", self)
end

def text()
    return @text
end

end

This is almost deceptive, in that the most complex aspect of **MyModel** is the use of the **NSNotificationCenter**. One hopes that in the real world, there are all sorts of domain objects. In case it's not obvious, **NSNotificationCenter.postNotificationName\_object\_()** is the complement to **NSNotificationCenter.addObserver\_selector\_name\_object\_()** discussed previously.

So we've got a simple Model that concerns itself with problem domain and a simple View that concerns itself with output. So now let's return to the initialization sequence and the Controller, which concerns itself with input.

::: {#codeSnippetWrapper style="line-height: 12pt; background-color: #f4f4f4; margin: 20px 0px 10px; width: 97.5%; font-family: 'Courier New', courier, monospace; max-height: 200px; font-size: 8pt; overflow: auto; cursor: text; border: silver 1px solid; padding: 4px;"}
``` {#codeSnippet style="line-height: 12pt; background-color: #f4f4f4; margin: 0em; width: 100%; font-family: 'Courier New', courier, monospace; color: black; font-size: 8pt; overflow: visible; border-style: none; padding: 0px;"}
#This is in class MyController
def initWithView_model(view, model)
    @view = view
    @model = model
    #View is manipulated by Controller
    @view.window.setDelegate(self)

    #Controller listens for updates from Model via NSNotification Center
    @notification_center = OSX::NSNotificationCenter.defaultCenter()
    @notification_center.addObserver_selector_name_object_(self, :on_model_updated, 'MyModelUpdatedNotification', @model)

    return self
end

:::

Here, we see again the specification of a callback for when the Model notifies of changes. In this case, I just made an function that simply logged itself to the console ("Does input care if the text changes? Mmm... Not really..."). Really the only important piece of code here is the attachment of the view's window's callbacks to the MyController. To fill that out, I had to write some simple functions to handle events associated with the window closing and the application terminating.

Easy-peasy, and here is the program running:

picture-4