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.
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.
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.
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:email@example.com:/cvsroot/rubycocoa login $ cvs -z3 -d:pserver:firstname.lastname@example.org:/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.
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
NSString classes provide the
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:
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 "
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
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.
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.
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,
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.|
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 un
tar, 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.|
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
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 (
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.
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.
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
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)
fileTypeView, and 5)
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)
extractArchive, and 5)
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
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.
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
Controller object. Let's start with the actions
for the '+' and '-' buttons.
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.
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
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
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.
browseForArchive action allows the user to
select an archive file for extraction using an
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
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.