Your first few days on RubyCocoa

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 newcocoa gem
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 newcocoa command

newcocoa HelloWorld

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


  open English.lproj/Main.nib 

Subclass NSObject to create your controller
Subclass NSobject

Name the new Class “Controller”

Right-click on the newly created controller and instantiate it
Instantiate Controller
This gives you a cool icon in the “Instances” tab.

Add a button and text control to your window
Drag Button
(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”.
Outlets

Link the controller’s outlets and action to the window
Linking Actions and Outlets
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

run newcocoa -c
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.

Edit Controller.rb
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:


@textString.setStringValue("HelloWorld")

inside of def sayHello. Pretty cool huh?

Run Rake. Click your button. Rejoice.

That’s it!

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!


Extra Credit.
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__