MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Programming With Cocoa
An Introduction to RubyCocoa, Part 1

by Christopher Roach
10/05/2004

This article, and the second installment that follows next week, can be considered the fourth and fifth in a series covering Ruby programming on Mac OS X. However, unlike the first three articles of this series, this tutorial can be used as a standalone piece. You only need some knowledge of the Ruby programming language with a little prior experience in Xcode to understand the content found here.

That being said, if you wish to pursue Ruby programming further, feel free to check out my first three articles in the series on GUI programming with Ruby/Tk. Also, if you don't have much experience with Ruby, my first article is a handy tutorial of some of the basics. So, with all of that out of the way, let's jump into today's text.

What is RubyCocoa?

RubyCocoa is a framework that provides a bridge between the Ruby programming language and the Cocoa framework of the Mac OS X operating system. This framework allows you to create Mac OS X native, Cocoa-based applications using Ruby. It was created by Hisakuni Fujimoto and is currently in version 0.4.0. It seems to be pretty stable for application development.

In this article I hope to demonstrate the uses of this framework through the creation of a simple GUI for the Unix tar program. First, I'll cover the installation of the RubyCocoa framework, then some of the basics of using the framework. Finally, I'll demonstrate how to create the tar GUI using Xcode, Ruby, and the RubyCocoa framework.

O'Reilly Mac OS X Conference

If all goes well, when you're finished with this tutorial, you'll have both a working knowledge of developing Cocoa-based applications with Ruby, and a functioning application for creating and extracting tarred and compressed files.

Getting and Installing RubyCocoa

I wrote this article, and the program for it, on my PowerBook G4 running Mac OS X 10.3.5, and as such I have based my installation instructions on these parameters. If you are running the Jaguar version of OS X, you may want to try installing RubyCocoa using the disk image provided here. However, if you are running Panther, as I am, you may find it necessary to download and install the latest Panther version from the CVS repository. To do this, open up an instance of the Terminal application and type in the following commands, ignoring the dollar marks ($), of course.

$ cvs -d:pserver:anonymous@cvs.sf.net:/cvsroot/rubycocoa login
$ cvs -z3 -d:pserver:anonymous@cvs.sf.net:/cvsroot/rubycocoa co \
      -P -r branch-devel-panther -d rubycocoa-panther src
$ cd rubycocoa-panther
$ cvs update -d -P

Once you have the Panther version of the RubyCocoa framework, you can install it by typing in the following commands at the same command prompt (make sure you are still in the rubycocoa-panther directory created during the CVS session).

$ ruby install.rb config
$ ruby install.rb setup
$ sudo ruby install.rb install

The best way to find out if everything installed correctly is to open up Xcode and select "New Project" from the "File" menu. You should see in the "New Project" dialogue a couple of choices under "Application" with the titles "Cocoa-Ruby Application" and "Cocoa-Ruby Document-based Application." If so, you've installed everything correctly. You can cancel out of the dialogue and read on to get the basics down.

RubyCocoa Basics

The RubyCocoa framework forms a bridge between the Cocoa classes in Objective-C and their Ruby representations. All of the classes available in the Cocoa framework can be found in the RubyCocoa module OSX, which can be mixed-in to our classes to give them access to everything the module provides.

Methods in Objective-C have a strange syntax to those of us used to just about any language outside of Smalltalk. It uses spaces and colons to denote the different parameters that will be passed into the method at invocation. Thus, a typical Objective-C method call looks like the following code sample:

[oPanel runModalForDirectory:self file:nil types nil]

In RubyCocoa it can be rewritten using two different techniques. The first replaces all of the colons and spaces with underscores so that the same method looks like the following in Ruby:

oPanel.runModalForDirectory_file_types(self, nil, nil)

The other technique can be used to make a little more sense of method names that are extremely long. Using this technique, the method name consists of everything in the Objective-C method's name up to the first detached parameter name. The rest of the parameter names are moved into the argument list. Thus, every argument after the first is prefaced with another argument that is a Ruby symbol with the same name as the parameter it represents. It sounds confusing, I know, but the example below shows you how easy it is once you get used to the syntax.

oPanel.runModalForDirectory(self, :file, nil, :types, nil)

Another gotcha that you'll come across when using RubyCocoa is in the values returned by the Cocoa methods. Cocoa methods will return a Cocoa value and not its Ruby counterpart. Thus, when a method returns a string or an array, it is returning an NSString or an NSArray and not their Ruby equivalents. In order to manipulate these properly you will need to convert them into their Ruby counterparts. The NSArray and NSString classes provide the to_a and to_s methods respectively. These methods should be called when a Ruby array or string is needed.

Predicate methods (methods that return a Boolean value) can also foul up the works a bit. When calling a predicate method found in a RubyCocoa class, the method will return a 0 or 1, which both evaluate to true in Ruby. In order to avoid any mix-ups when calling a predicate method, you should suffix the method name with a question mark (?). Doing this will make the method return a Ruby Boolean value and will make the method name look like the following snippet of code:

oPanel.allowsMultipleSelection?

Finally, one last problem you may run into when calling RubyCocoa methods is when you come across a method with a name that conflicts with a Ruby method. When this happens, just prefix the RubyCocoa method with "oc_".

When instantiating RubyCocoa classes you will use the same methods that you do in Cocoa with Objective-C rather than the new method supplied by Ruby classes. Thus, an instance of the NSObject class can be created with the following code:

obj = NSObject.alloc.init

Even though you use Cocoa methods to create Cocoa objects, it is not necessary to use methods such as release, autorelease, and retain to manage the memory allocated to each object since Ruby performs garbage collection for all of its objects.

Anything else you may need to know when writing RubyCocoa-based applications can be found on the RubyCocoa programming page.

So, now that you've got the basics of creating RubyCocoa applications out of the way, let's move on to the fun stuff. The next section will cover creating our GUI wrapper for the tar program. So, get Xcode running (if you don't still have it running) and read on.

Creating the Interface

The first step toward creating our GUI wrapper for the tar program is creating a new Cocoa-Ruby Application in Xcode. You begin just as we did when testing our RubyCocoa install. Select "New Project" from Xcode's "File" menu and choose "Cocoa-Ruby Application" in the "New Project" dialogue. Once you've selected the correct application type, give your new project a name (I called mine "RubyCocoaTar") and select its location in the file system, and voila! We have a new RubyCocoa application. Doesn't do much right now, but we'll remedy that shortly.

After creating a new RubyCocoa project, we need to create our GUI using Interface Builder (IB). We do this just as we would for an Objective-C or Java Cocoa-based application. You double click on the "MainMenu.nib" file under the "Resources" folder in Xcode. This will open Interface Builder with a new Window ready for us to alter.

The first thing to do is to remove the unnecessary menus and rename the main window and all of the remaining menu items. In my application I've only kept the "NewApplication" menu (although I renamed it to "RubyCocoaTar" to match my application's name), and I got rid of the "Preferences" option underneath that menu. After renaming the rest of the items under the "NewApplication" menu to match the name you've given your project, you can proceed on to crafting the application's GUI.

You're going to create an application with a tab-view interface, with one tab for creating our tar files and another tab for extracting already existing tar files. So, you'll need to click on the "Cocoa-Containers" palette and drag the NSTabView over to the main window. Resize the NSTabView instance and rename the tabs to "Create" and "Extract" so that, when finished, your window looks like the image below.

Figure 1. NSTabView added to the main RubyCocoaTar window.  Figure 1. NSTabView added to the main RubyCocoaTar window.

Once you've got the NSTabView object set up properly, you'll need to add the rest of the components to each of the two tabs. First, we'll add the necessary components to the "Create" tab. We're going to keep our main interface about as simple as possible. For the "Create tar file" portion of our interface we'll only need an NSTableView object for displaying the files we've chosen to tar and compress, and three buttons for adding, removing, and tarring files.

Go ahead and add each of these items to the "Create" tab so that it looks like the image below. Make sure that the NSTableView only has one column, and rename that column to "Files".

Figure 2. "Create" tab for the RubyCocoaTar application. Figure 2. "Create" tab for the RubyCocoaTar application.

I like to get my interface fully set up before creating my controller class and adding the connections. So, why don't we go ahead and add the GUI elements to the "Extract" tab and then come back and create our controller class and all of its connections.

Considering how simple our "Create" tab interface is, it's hard to believe that the "Extract" tab could be any simpler, but guess what — it is. The "Extract" tab only needs an NSTextField to hold the name of the file we wish to decompress and untar, a button for calling up an NSOpenPanel to locate the file, and a button to call our extraction method, plus a label and separator to finish out the package. After adding these elements to our interface, the "Extract" tab should look like the image below.

Figure 3. "Extract" tab for the RubyCocoaTar application. Figure 3. "Extract" tab for the RubyCocoaTar application.

Well, we've added all of the major elements to our tar GUI and are just about ready to create our controller class and start adding some connections to our interface. However, we have one final, minor, GUI element to create.

There are three types of compression that we are going to focus on in our application, and thus, with the inclusion of tarring without compression, we will have four different file types: .tgz, .bz2, .Z, .tar. This NSSavePanel also needs to provide the user with a way of choosing the type of tarred file they wish to create (e.g., .tgz, .bZ2, .tar, or .Z). In order to do this, we'll need to create a new NSView object that houses an NSPopUpButton containing a list of file extensions to be used with the NSSavePanel. So, let's go ahead and get our new NSView object created, and a bit later we'll find out how to add it to the NSSavePanel.

To create our file types view, we'll need to add an instance of the NSView class to our nib file. Click on the "Cocoa-Containers" palette and drag the NSView over to the Instances tab of the "MainMenu.nib" window. This should display a new window with the title "View" in which we will add our file types drop-down list. If not, simply double click on the instance of NSView you dragged over to the "MainMenu.nib" window and the "View" window should display. You can change the name of the view if you like, although it's not absolutely necessary, since we are not really as concerned with the NSView object as much as we are the file types drop-down list.

Once we have a window open displaying our custom view, we can add the label and drop-down list (NSTextField and NSPopUpButton respectively) to it. In our application we are going to give the user the option to tar a group of files either with or without compression.

There are three types of compression that we are going to focus on in our application and thus with the inclusion of tarring alone, we will have four different file types: .tgz, .bz2, .Z, .tar. Since the NSPopUpButton provides us with only three menu items by default we'll have to add one more menu item to the component. To do this, drag-and-drop a "Menu Item" from the "Cocoa-Menus" palette onto the new NSPopUpButton item in our custom view. Once we have four menu items, we can go ahead and change each one to one of the four file extensions mentioned above.

Once finished, you should have a custom view that looks like the view in the following image.

Figure 4. Custom view for selecting the file type.
Figure 4. Custom view for selecting the file type.

That takes care of the GUI design for our system. Now that we have the GUI entirely laid out, we can move on to creating the controller class for the GUI and assigning actions and outlets to it. The next section deals with this step.

Creating the Controller Class

Just like a normal Objective-C controller class in a Cocoa application, our Ruby Controller class must inherit from the NSObject class. So, our first step is to subclass the NSObject class. Find the NSObject class in the "Classes" tab of the "MainMenu.nib" window.

Select this class in the list and press the "Return" button. This should create a new subclass of the NSObject class called MyObject. Highlight the new NSObject child class, if it is not already highlighted, and change its name from MyObject to Controller.

Once we've created and renamed our new Controller class, we need to add the necessary actions and outlets to it. Make sure the Controller class is highlighted and open the Info window (you can do so by selecting the "Show Info" item under the "Tools" menu, or by using a "Shift-Command-I" key combination) and select "Attributes" from the drop-down list.

First we'll add the outlets. So, click on the "Outlets" tab and add five new outlets to the list: 1) fileTableView, 2) archiveFile, 3) fileType, 4) fileTypeView, and 5) mainWindow.

After adding all of our outlets to our Controller class, we need to add the actions that it will be tasked with performing. To do this, we click on the Actions tab and add the following actions to the list: 1) addFile, 2) removeFile, 3) createArchive, 4) extractArchive, and 5) browseForArchive.

We now have our Controller class fleshed out enough to let us start making all of our connections between the nib file we've created and our Ruby application. Before we proceed, however, we must create an instance of our Controller class. We do this by selecting our Controller class from the list under the "Classes" tab in our "MainMenu.nib" window. Then, you can either control-click on it and select "Instantiate Controller" or you can select it through the "Classes" menu in the Interface Builder menu bar.

Once we have an instance of our Controller class, we can begin making all of our necessary connections. In the process we'll also find out what each of the outlets and actions do in our application. We'll begin with the outlets we created and then move on to the actions afterward.

The first three outlets are used to update and access the information the user shares with us through the interface. The latter two are needed only for using the NSOpenPanel and NSSavePanel classes. You'll need to connect each of the outlets to their respective interface elements. This is done by control-clicking on the newly created instance of our Controller class and dragging a line from it to the GUI element it represents.

The fileTableView outlet should be connected to the NSTableView object under the "Create" tab that displays the files we have selected for archiving. The archiveFile outlet should point to the NSTextField (text box, not label) under the "Extract" tab that will hold the name of the archive file we have chosen for extraction. The fileType needs to be connected to the NSPopUpButton that we placed in our custom view in order to allow us to access the file type chosen by the user when in the process of creating a new archive file. The fileTypeView outlet is a pointer to our custom view we created for the NSSavePanel, and finally, our mainWindow outlet is a pointer to the main window of our application.

You now have half of your Controller class' connections created. Next, we'll need to attach each of its actions to an element of the interface. Let's take a look at what each is supposed to do, and to which item each will be attached.

Connecting actions to the GUI elements that trigger them is done slightly differently than connecting outlets to their GUI elements. Rather than control-clicking the Controller object and dragging the line to the GUI element, we are going to go backwards, and control-click the GUI element that triggers the action and drag a line to the Controller object. Let's start with the actions for the '+' and '-' buttons.

The addFile action calls up an instance of the NSOpenPanel and allows the user to select one or more files that they want to include in their archive. The removeFile action simply deletes the currently selected file from the table. We need to connect both of these actions to the '+' and '-' buttons underneath the NSTableView object in our "Create" tab.

The createArchive and extractArchive actions are responsible for creating new archive files and extracting the contents of an already existing archive file to a chosen directory. The first action displays an NSSavePanel allowing the user to select a location for—and assign a name to—the archive file being created.

We should create a connection between it and the "Create Archive" button on the "Create" tab using the same method that we did for our add and remove file actions. The extractArchive action executes the tar program to extract the selected archive file to the directory chosen by the user through an instance of the NSOpenPanel class. The "Extract" button should be connected to the extractArchive method in the same way as the "Create Archive" button.

Finally, the browseForArchive action allows the user to select an archive file for extraction using an NSOpenPanel object. It is called whenever a user clicks on the "Browse" button under the "Extract" tab, and therefore, needs to be connected to that button following the same routine as described earlier.

That takes care of about everything that we can do using Interface Builder. The rest of our work will need to be done in Xcode. Normally, at this point we would select our Controller class and have Interface Builder create files with skeleton code for us to fill out in Xcode. However, it will only do this for Objective-C and Java, not Ruby. So, we'll have to do a little bit of work by hand here. So make sure you've saved your nib file before closing Interface Builder and Xcode.

The next installment will deal with creating the skeleton code that Interface Builder usually creates for us and finishing our project by adding in the rest of the Ruby code needed to give us a functioning application.

Until then, I hope this tutorial keeps you busy!

Christopher Roach recently graduated with a master's in computer science and currently works in Florida as a software engineer at a government communications corporation.


Return to MacDevCenter.com.

Copyright © 2009 O'Reilly Media, Inc.