4. Interactive RubyCocoa

Let’s use our interpreter to explore a small Cocoa application.

In Chapter 2 of Cocoa Programming for Mac OS X, Aaron Hillegass built a simple demonstration with Interface Builder and Objective-C. Now I’m going to show you the same application done entirely in Ruby. It’s actually a lot easier this way. Instead of tediously taking you step-by-step through Interface Builder, I’ll just lay it out in front of you to review.

Here it is. Save it as random.rb in your project directory.

Pure Ruby RandomApp [ruby]
# Aaron Hillegass' RandomApp in 100% Ruby  
# to see it the old-fashioned way, see Chapter 2 of Aaron's book:
# "Cocoa Programming for Mac OS X, 2nd Edition"

class RandomAppWindowController < OSX::NSObject
  attr_accessor :seedButton, :generateButton, :textField, :window

  ns_overrides :init
  def init
    styleMask = OSX::NSTitledWindowMask + OSX::NSClosableWindowMask +
      OSX::NSMiniaturizableWindowMask
    @window = OSX::NSWindow.alloc.initWithContentRect_styleMask_backing_defer(
      [300,200,340,120], styleMask, OSX::NSBackingStoreBuffered, false)
    @window.setTitle 'RandomApp'
    
    @view = OSX::NSView.alloc.initWithFrame @window.frame

    @seedButton = with(OSX::NSButton.alloc.initWithFrame([20,75,300,25])) do |b|
      b.setTitle "Seed random number generator with time"
      b.setAction :seed
      b.setTarget self
      b.setBezelStyle OSX::NSRoundedBezelStyle
      @view.addSubview b
    end

    @generateButton = with(OSX::NSButton.alloc.initWithFrame([20,45,300,25])) do |b|
      b.setTitle "Generate random number"
      b.setAction :generate
      b.setTarget self
      b.setBezelStyle OSX::NSRoundedBezelStyle
      @view.addSubview b
    end

    @textField = with(OSX::NSTextField.alloc.initWithFrame([20,20,300,20])) do |t|
      t.setObjectValue OSX::NSCalendarDate.calendarDate
      t.setEditable false
      t.setDrawsBackground false
      t.setAlignment OSX::NSCenterTextAlignment
      t.setBezeled false
      @view.addSubview t
    end

    @window.setContentView @view
    @window.center
    @window.makeKeyAndOrderFront self
    self
  end

  def seed(sender)
    srand Time.now.to_i
    @textField.setStringValue "generator seeded"
  end

  def generate(sender)
    @textField.setIntValue(rand(100) + 1)
  end
end

def with(x)
  yield x if block_given?; x
end if not defined? with

You can probably understand everything just by reading the code. When it is initialized, the window controller creates an NSWindow with an NSView as its content view. Then it inserts two buttons and a text field at specified locations. I had to work out the locations manually; Interface Builder would do that for us. I also had to set the buttons’ bezel styles to match what we would get in Interface Builder. But in return, everything that our application needs is here in one file. There are no classes, outlets, or actions to declare and no connections to make (or forget to make).

If this was a standalone application, I’d have an application delegate instantiate our controller in response to the applicationDidFinishLaunching message. But for our purposes, it will be enough to simply create it from our interactive command line.

Now let’s run this little application from our console. First, create a window. Restart your application and type the following in the console:
r = RandomAppWindowController.alloc.init

The window will appear onscreen. Click on the buttons to confirm that everything works.

Our controller has attributes that allow us to access the buttons. Type this to see the generate button:
r.generateButton
Now turn off its outline.
r.generateButton.setBordered false
You can also change its title.
r.generateButton.setTitle "press me" 
In the NSButton documention (look it up!), we see that we can also give the button an alternate title that it will display when it’s been pressed. Let’s do it.
r.generateButton.setAlternateTitle "do that again" 
But when we press the button, the title doesn’t change. Back in the documentation, we see that we have to change the button type.
r.generateButton.setButtonType OSX::NSToggleButton

That’s better.

Now look at the actions for each button.
r.generateButton.action
r.seedButton.action
Swap them.
r.generateButton.setAction "seed" 
r.seedButton.setAction "generate" 

Did you verify that? Now put them back and we’ll do something even better.

Because Ruby is so dynamic, we can open up the window controller class and redefine one of our button actions. Enter the following in the console:
class RandomAppWindowController
  def generate(sender)
    @window.setTitle "ruby roolz" 
  end
end

Now our button presses set the window title.

To recover the original behavior, reload the source file.
load "random.rb" 

Did you get all that? You can incrementally add and modify your Ruby code and reload it on the fly.
Is there a better way to learn Cocoa than that?

Hmm… how about by reading example applications written in pure Ruby?

Did you find an error? Is something missing? Post your comment or suggestion below!

Comments (8) post
  1. Greg Sat Oct 28 10:00:35 +0000 2006

    This whole project is hugely useful, especially as you say for getting to know your way around Cocoa. Being able to inspect and drive your objects from a console … just, wow.

    My knowledge of the Tao of Cocoa runs out fairly quickly, but I wonder if there might be a way to use nib files created in Interface Builder for the classes you can load from this console. I tried it by placing a custom-named nib file in the Contents/Resources/English.lproj folder, and I can load it (with NSBundle.loadNibNamed_owner()), but can’t seem to wake it up. I also tried creating an NSNib object from the nib-file and manually walking it through the steps of “waking up.” Problem is, I don’t know what that involves.

    I get about 5 different error messages (with different methods in place of “terminate”) like this one:

    Could not connect the action terminate: to target of class AppController

    Any clues?

  2. Greg Sat Oct 28 10:30:17 +0000 2006

    Here’s a solution! I added a loadMyNib method to my class and copied the nib file I made in IB into rubyapp’s bundle. In my case, the application’s main class and main nib file are called the same (“AppController(.rb|.nib)”):

    def loadMyNib
      dict = NSDictionary.dictionaryWithObjectsAndKeys([self, "NSOwner"])
      NSBundle.bundleForClass(self.bundle).loadNibFile_externalNameTable_withZone("AppController", dict, self.zone)
    end
    

    Now you don’t have to write loads of view-initializing code just to use this console. Hope someone will find this helpful. Note: my main class still has an awakeFromNib method. loadMyNib just gets things … well, loaded. In the console, then, I just do the following:

    appCon=AppController.alloc.init
    appCon.loadMyNib

  3. Tim Burks Sat Oct 28 11:07:39 +0000 2006

    Greg, thanks for sharing your discoveries.

    I expect that any Cocoa API function for loading nib files will work. Here’s an exerpt from some code I wrote that loads a nib in a different way:
    class DemoController < ObjC::NSWindowController
      def init
        initWithWindowNibPath_owner_("/Users/tim/Desktop/demo.nib", self)
        showWindow_(self)
        self
      end
    end
    
  4. Tim Perrett Wed Apr 18 05:51:36 +0000 2007

    Hey Tim

    Its interesting you say that any cocoa nib loading shoud work, but im trying to call a nib sheet using:

    NSBundle.loadNibNamed(‘theNib’)

    but it claims loadNibNamed is not a valid method? I was reading an apple tutorial on custom sheets – http://developer.apple.com/documentation/Cocoa/Conceptual/Sheets/Tasks/UsingCustomSheets.html

    am i missing somthing about loading the nib?

    Cheers

    Tim

  5. Tim Burks Wed Apr 18 08:37:49 +0000 2007

    Tim,

    It’s easy to miss it, but there’s more to that selector. It also expects an owner for the nib. Try this instead:

    NSBundle.loadNibNamed_owner(“MyCustomSheet”, self)

  6. Tim Perrett Sun Apr 22 06:02:45 +0000 2007

    Ah brilliant, the NIB loads fine now, but how do you actually call it?

    The cocoa tutorial implements:

    [NSApp beginSheet: myCustomSheet modalForWindow: window modalDelegate: self didEndSelector: @selector(didEndSheet:returnCode:contextInfo:) contextInfo: nil];

    This is one of the things that always confuses me about rubycocoa, how do you translate a big method call like this? Do I need to do somthing like:

    NSApp.beginSheet_modalForWindow_modalDelegate(?,?,?)

    Cheers

    Tim

  7. John McIntosh Tue Feb 05 19:22:42 +0000 2008

    In 10.15.1, when I reload “random.rb”, I get the following message:

    8:ns_overrides is no longer necessary, should not be called anymore and will be removed in a next release. Please update your code to not use it.

    This seems to me to indicate that irb needs to be updated. Is that right?

  8. Michael Black Mon Feb 25 10:19:08 +0000 2008

    John, you can safely remove the ‘ns_overrides’ line. It’s no longer required by the version of RubyCocoa that ships in Leopard.