4. Interactive RubyCocoa
4.1 Pure Ruby RandomApp
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.
# 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.
4.2 RandomApp Live
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.generateButtonNow turn off its outline.
r.generateButton.setBordered falseYou 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.actionSwap 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
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?
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)”):
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:
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 endHey 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
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)
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
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?
John, you can safely remove the ‘ns_overrides’ line. It’s no longer required by the version of RubyCocoa that ships in Leopard.