I’ve been playing with RubyCocoa lately and really enjoying how easy it is to create OS X GUI apps with just a few lines of Ruby. After playing with this stuff for the past few days, I’m even more excited about Apple’s inclusion of Ruby for Cocoa Development officialy in Leopard.
This post will walk you through the first steps into the RubyCocoa world (frankly, I’m not much beyond the first steps myself!). Before we get to the codin’ let’s get everything set up.
Step 1: Get your Ruby Ready RubyCocoa requires the
--enabled-shared configure flag (“build a shared library for Ruby.”) to work from the command line. I’m going to assume that if you’re reading this (and following along), you’re not using OS X’s default Ruby binary (1.8.2). I’m going to also assume you have the ruby source tree somewhere on your harddrive (~/src maybe?). If you have a different setup, adjust accordingly. Go into your source tree and re-configure with your previous
./configure command (which can be found at the top of config.log) while adding –enable-shared to it. Here’s how mine looked:
./configure --prefix=/usr/local --enable-pthread --with-readline-dir=/usr/local --enable-shared
When that’s all done, run
make followed by
sudo make install.
Step 2: Install RubyCocoa
Since your Ruby is in a different location than the OS X default (/usr/local/bin instead of /usr/bin), you need to compile RubyCocoa from source instead of using the binary release. Fetch the latest RubyCocoa version from source forge (0.11.0 as of this writing – 2007-05-28), or grab it from the RubyCocoa SVN.
Then configure, compile and install RubyCocoa:
$ ruby install.rb config $ ruby install.rb setup $ sudo ruby install.rb install
Step 3: Install the
Jumping head-first into the world of Xcode (and away from my beloved TextMate) was a bit too much for me. Justin Palmer’s pointer to newcocoa was the final piece I needed to really get into RubyCocoa. Get it like you would any other gem:
sudo gem install newcocoa
With all that out of the way, let’s get started with our first little app.
I made a short screencast of myself making this app.
Create the project with the
Edit the Rakefile. By default the Rakefile generated with newcocoa has targets to ppc and intel. I haven’t figured out how to make a universal binary yet, but for now remove the -arch (ppc in my case, this was made on a MacBook) line you don’t have.
cd HelloWorld mate Rakefile
Edit the .nib with Interface Builder
Subclass NSObject to create your controller
Name the new Class “Controller”
Right-click on the newly created controller and instantiate it
This gives you a cool icon in the “Instances” tab.
Add a button and text control to your window
(The text control is the same method – on the Cocoa-Text tab drag a “System Font Text” over to your window)
Double click your new button and change the text to “Hello World”. Then double click your text control and remove all the text.
Add 2 outlets and 1 action to your controller. Open the inspector window ( ⌘ ⇧ I) and select the connections page (⌘ 2). Double click the instance of your controller in the Main.nib floating window (it should be a blue cube). Under actions hit Add and type “sayHello:”. Then in the Outlets page add “helloButton” and “textString”.
Link the controller’s outlets and action to the window
While holding down control, click on the Controller instance (blue cube) and drag up to your button. Then double click the “helloButton” outlet. Repeat for the text field (and double click textString). Then, while holding control, click on the Hello Button and drag down to your Controller instance. Now double click sayHello: under actions. This is how you define the connections between an interface (the VIEW) and an instance (the CONTROLLER). (Sidebar: Where are models? We’re not that far yet!)
Save your nib and go back to your terminal
This command inspects your NIB files for subclasses and linked actions/outlets and creates skeleton files for them. Since you named your controller as “Controller”, you now have Controller.rb in your project directory.
Here is where the bulk of your code will go (or in this project, all of it). You have access to the full Ruby / Cocoa bridge, which has most (all?) of the cocoa API methods available. The only code you’ll be adding is:
inside of def sayHello. Pretty cool huh?
Run Rake. Click your button. Rejoice.
I like to clean up the generated controller file like so:
require 'osx/cocoa' include OSX class Controller < NSObject ib_outlets :helloButton, :textLine def sayHello(sender) @textLine.setStringValue("Hello, world!") end def awakeFromNib end end `rake` if $0 == __FILE__
The last line let’s me hit ⌘ R inside of textmate to launch my project -Agile!
Subclass NSWindow (call it MyWindow) and then set your windows “Custom Class” to “MyWindow”. Run
newcocoa -c again (it skips Controller.rb this time) you’ll now have MyWindow.rb. Putting the following code in there will implement an “edge-snapping” effect.
require 'osx/cocoa' include OSX class MyWindow < NSWindow def awakeFromNib @moving = false NSNotificationCenter.defaultCenter.addObserver_selector_name_object( self, :window_moved, "NSWindowDidMoveNotification", self ) end def window_moved(notification) unless @moving @moving = true pillow = 10 # how far from the edge before the "snap" # 0,0 in os x is the LOWER LEFT corner, not the upper left. # set up the objects to grab from origin = self.frame.origin window = self.frame.size screen = self.screen.frame.size # the ideals (set the allowances for messed up windows here) ideal_left = 0 ideal_bottom = 0 ideal_top = screen.height ideal_right = screen.width # current x and y x, y = origin.x, origin.y # current edges top = origin.y + window.height right = origin.x + window.width bottom = origin.y left = origin.x # the snapping code - set the paddings here x = ideal_left if ((ideal_left - 100)..(ideal_left + pillow)).include?(left) y = ideal_top - window.height if ((ideal_top - pillow)..(ideal_top + 100)).include?(top) x = ideal_right - window.width if ((ideal_right - pillow)..(ideal_right + 100)).include?(right) y = ideal_bottom if ((ideal_bottom - 100)..(ideal_bottom + pillow)).include?(bottom) setFrameOrigin(NSPoint.new(x, y)) if (x != origin.x or y != origin.y) @moving = false end end end `rake` if $0 == __FILE__