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:

:::

# 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:

:::

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)

:::

#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()":

#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!

:::

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.

:::

#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