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__

15 thoughts on “Your first few days on RubyCocoa”

  1. Great article! Hopefully we get more of these in due time. It should be noted that you can use TextMate without newcocoa, I still do on occasions. It requires a little more leg work, but it’s definitely possible.

  2. Cool! I’d love to see more. It’s not only the developers who are missing tutorials. :)

  3. Great article, fantastically helpful!

    2 gotchas:

    1) From the gcc man page:

    -arch arch
    Compile for the specified target architecture arch. The allowable values are i386, ppc and ppc64. Multiple options work, and direct the compiler to produce “universal” binaries including object code for each architecture specified with -arch. This option only works if assembler and libraries are available for each architecture specified. (APPLE ONLY)

    So it looks like the multiple -arch options in the rakefile are correct to build a fat binary (if you have all the various libraries in both archs)

    2) I needed to select the attribute page on the inspector, rather than connections to add my actions/outlets

  4. Alan, thanks for looking up the -arch thing. I gotta find the libs so I can make universal binaries if I ever decide to distribute my apps!

  5. Thanks for the screencast!

    I’ve written some simple OpenGL apps in Objective-C but have never used Ruby for Mac OS X apps. I have some ideas that I would like to try out.

  6. Awesome. I tried this a while back and not having developed cocoa apps before it took me a while to get a hello world app working from scratch on XCode and InterfaceBuilder. This is nice and simple. Thanks for pointing out the newcocoa gem.

  7. Neat run-though, Erik!

    Whilst i didn’t quite take the same path as you (as in, i downloaded the disk image and just used XCode), i still found it quite easy to pick up the RubyCocoa bindings. I was however a bit rusty on the Interface Builder stuff, but i more or less eventually figured it out.

    I was considering using the PyObjC bindings which are the Python equivalent of RubyCocoa, however after thinking about it i just decided to dive into RubyCocoa considering i have been doing a lot of RoR development (e.g. RailsCollab) recently. In a way i also enjoy Ruby more than Python, so i’ve more or less decided to stick with it for the while.

    In the future, be interesting to see how powerful RubyCocoa can be.

  8. Wicked Article.
    This has helped me get legs with cocoa (ok more like knees, buts legs will soon follow).

    I have managed to begin a simple relational db app using the models from a rails app ( Which I created for an active record demo within a rails presentation for my former technical school)

    I had hoped to demo some rubycocoa as well but i wasnt quite joining all the dots pilfering examples or harder still porting ideas from objc
    examples.

    So far I have managed to include all my models and initialize active record, do find and display operation with data from mysql

    Once my (barely started) site (dont look now its crap) it up and running I will post my endeavors grasping cocoa from ruby with lots of active record, (bloody AR junkie here, can’t get enough).

Comments are closed.