TweetFollow Us on Twitter

RubyCocoa-Part 1

Volume Number: 24 (2008)
Issue Number: 04
Column Tag: Programming


A new way to write Cocoa applications-Part 1

by Rich Warren

I admit it. I have a soft spot for Ruby. That probably comes as no surprise, if you've read my earlier articles (particularly Introduction to Ruby on Rails and Ajax on Rails, both available online at Needless to say, I'm quite giddy with Leopard's scripting language support. Leopard has elevated Python and Ruby to. . .um. . .not first class citizens. Not quite. But they make a strong second-class showing.

In fact, my biggest complaint comes from the terminology. Apple's own documentation refers to both Ruby and Python as scripting languages. Scripting Languages? Sure, they are both interpreted languages, but the word "scripting" makes them sound like limited, little things. Trust me, you can use these languages to do a lot more than just write scripts. We have two, full-blown, dynamic, object oriented programming languages, and Leopard puts their power at our fingertips.

New Ruby and Python Features

Ruby and Python are not new to OS X. Tiger shipped with both languages installed (though, if you've read my previous articles, you know that the Tiger version of Ruby kinda sucked). Leopard, however, kicks the support up a notch. They've invested a lot of time into getting the details right. While they may not always succeed, I appreciate the effort.

For example, Xcode comes with templates for a variety of Ruby and Python projects. Syntax highlighting and code completion work as expected. Most importantly, Leopard integrates both languages more tightly into the operating system. Both include a bridge to the Objective-C runtime, and both can communicate with scriptable applications.

The Bridge to Objective-C

Leopard ships with the popular RubyCocoa and PyObjC libraries already installed. Developers can use these libraries to write Cocoa applications in either Ruby or Python, respectively. Both languages have access to Leopard's core technologies, including Core Data, Bindings and Document-based applications. These libraries even support the new rock-star frameworks like Core Animation.

But, why would you want to use Ruby or Python? Some might say they're addictive; once you start using them it's hard to go back (trust me, I use Java for my day job). But, you can find other reasons as well. Both Ruby and Python are very expressive languages. You can get a lot of work done with very little code. This makes them ideal choices for rapid development and prototyping.

Additionally, Objective-C, Ruby and Python share many common concepts and design choices. They are all dynamic, object-oriented languages. Ruby and Objective-C in particular, were both heavily influenced by Smalltalk. This common ground helps us coordinate our code across the different languages.

And we can freely mix our code. We can use Ruby subclasses of Objective-C classes, or Python delegates for Objective-C objects. We can transparently call one language from the other. This gives us more power and more flexibility than any one language would have on its own. We have access to each language's libraries. We can exploit their individual strengths, using one language to spackle over the other's weaknesses.

Unfortunately, Cocoa seems to have a one-bridge-at-a-time rule. Mixing either Ruby or Python with Objective-C works just fine. But mixing Ruby and Python quickly becomes problematic. Both frameworks try to load the BridgeSupport dylib, and this can cause errors. Some developers have posted workarounds on the web, but they tend to feel rather hackish to me. Still, I think this issue will smooth itself out with future updates.

The Bridge to OSA

We can also use Ruby and Python to communicate with scriptable applications using the Open Scripting Architecture (OSA). RubyCocoa and PyObjC already give us full access to the native Scripting Bridge, but I think this often becomes unwieldy. We end up writing Ruby (or Python) versions of Objective-C calls on AppleScript APIs.

Fortunately, each language has its own library to simplify scripting: RubyOSA for Ruby and py-applescript for Python. Unfortunately, Leopard does not include these libraries. You need to install them on your own.

Ruby in Leopard

For the rest of this article will dig into the Ruby-specific additions to Leopard. Python has comparable features, but for simplicities sake, I will focus on what I know. Ruby comes ready for serious development. Leopard's installation includes several important libraries: rake, Mongrel, Ferret, Capistrano, sqlite3-ruby, dnssd (aka Bonjour) and Rails. Of course, given the frantic rate of Ruby development, many of these libraries have already grown long in the tooth. Still, that's not a huge concern. Leopard also includes RubyGems.

RubyGems is a command-line package manager for Ruby. It allows us to quickly and easily install and update Ruby libraries. For example, to update the current version of Rails, just type:

gem update –include-dependencies rails

However, if you're like me, the thought of wildly upgrading your system libraries makes your stomach churn. What happens if something goes wrong? Sooner or later, something always goes wrong. Won't this just screw up my system?

Well, put down that bottle of Pepto. Leopard carefully separates its pre-installed libraries from the user-installed libraries and updates. Accidentally updating to an unstable version doesn't change your original system files. Simply uninstall the offending library, and you're good to go. This also makes rolling back to factory defaults quite easy. Simply delete the user-gems folder.

Leopard keeps built-in libraries in the /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/gems/1.8/ folder. The gems subfolder contains the actual libraries, while the doc subfolder contains both ri and html documentation.

When you run gems, it saves new libraries to the /Library/Ruby/Gems/1.8/ folder. Again, you can find the libraries in the gems folder, while documentation is. . .wait for it. . .in doc.

Just to be complete, Leopard stashes the RubyCocoa files in a third location: /System/Library/Frameworks/RubyCocoa.framework

I highly recommend poking around in these directories–particularly the RubyCocoa header files. They can give you a good feel for the breadth of options available.

The Limits of RubyCocoa

Of course, there are no magic bullets, and RubyCocoa has its share of downsides.

Slow, slow, slow

As much as I love Ruby, it is a fairly slow, interpreted language. RubyCocoa code will run significantly slower than equivalent Objective-C code. Depending on the application, this may not be a problem. After all, GUI applications spend most of their time waiting on the user anyway.

Besides, if a RubyCocoa program feels slow, you can always profile it and look for bottlenecks. Once you identify likely problems, you can either redesign your code to eliminate the bottleneck, or convert it into faster, Objective-C code.

Finally, the newly released Ruby 1.9 uses a new, faster virtual machine. Unfortunately, as I write this, Ruby 1.9 only comes as a development release–it's not quite ready for prime time.

Not thread safe

Ruby 1.8 is not thread safe. You cannot call Ruby code on multiple native threads. To prevent possible problems, the bridge actually reroutes all Ruby calls from Objective-C to the application's main thread. However, as we will soon see, you can still use Ruby's threads within your Ruby code, which gives us a partial workaround. Again, the production release of Ruby 1.9 should fix this.

Xcode's debugger does not work

You cannot use Xcode's debugger on your ruby code. However, you can use Ruby's debugging tools along with new Leopard tools like DTrace and Instruments. This isn't an ideal solution, but it works.

RubyCocoa does not support Objective-C garbage collection

To me, this was probably the most disappointing limitation. Ruby itself uses garbage collection, but your Objective-C code must continue to manage its own memory. Somehow this just feels wrong.

Finding Documentation and Getting Help

Apple has included a number of documents and examples to help you get started. You can find the following articles linked off the "Introduction to Ruby and Python Programming Topics for Mac OS X" web page (

Ruby and Python on Mac OS X

Building a RubyCocoa Application: A Tutorial

Using Scripting Bridge in PyObjC and RubyCocoa Code

The Leopard Technology Series for Developers also includes a nice introductory article at h

However, if you want documentation about the frameworks that RubyCocoa supports, prepare for disappointment. You might find a promising folder at /Developer/Documentation/RubyCocoa. Unfortunately, this only contains a few files in Japanese. The actual RubyCocoa documentation is missing. Fortunately, we can fix this. . .more or less.

You need to download the latest RubyCocoa source release from Untar the source files, then run the following commands:

ruby install.rb config
ruby install.rb doc

This will create ri and html documentation for most of the Cocoa libraries supported by RubyCocoa. However, the documentation has two small problems.

First, it does not cover all the libraries that RubyCocoa supports.

Second, and more importantly, the installer tends to break whenever Apple updates their reference libraries. The RubyCocoa team tries to keep up with the latest changes, but they are chasing a moving target. The 0.13.0 release will work fine for a fresh install of Xcode 3.0, but if you've updated your reference libraries, it will fail. In that case, try the latest build from the SVN trunk using the following command:

svn co\
/src rubycocoa

Don't be surprised when you see errors while parsing Apple's documentation. RubyCocoa should still create documentation for most Cocoa classes.

Alternatively, you can simply look up the Cocoa classes directly from Apple's reference library. As we will see, you can easily translate an Objective-C method into a RubyCocoa call.

Even with all the tutorials, introductory articles and reference libraries, RubyCocoa has a number of dark corners. Fortunately, you can find several other resources to help you master RubyCocoa–or at least help you ask intelligent-sounding questions.


Leopard's developer tools include 40 sample projects for RubyCocoa. You can find these in the /Developer/Examples/Ruby/RubyCocoa directory. These samples range from old standbys (yet another Currency Converter) to video games. Take some time to browse these projects. They can give you a real feel for using RubyCocoa effectively.

Web Sites

While a quick search on Google brings up 370,000 matches for "RubyCocoa", I highly recommend two sites: the RubyCocoa project pages at ( and RubyCocoa Resources ( Both provide a range of useful articles. The introductory topics help you get started, while the advanced topics keep you coming back for more.

The Last Resort

The RubyCocoa community has an active mailing list. In my experience, everyone is helpful and kind. But, please: don't waste their time. Try to research the issue on your own. Then, if you're still stuck, check out RubyCocoa Talk.

You can subscribe to RubyCocoa Talk at

Our Project

To really understand something, sometimes you need to just jump in. Therefore, the rest of this article, will focus on building a simple RSS reader using RubyCocoa.

Why another RSS reader? Leopard already comes with built in RSS features for both Safari and Mail, not to mention many third-party applications. Still, I wanted to try something a bit messier than the typical toy project. By tackling a problem with rough edges, we get a better feel for RubyCocoa's strengths and weaknesses.

Additionally, I wanted a project that would demonstrate the following four points:

The project should use a RubyGem library.

The project should use key Cocoa technologies, like Core Data and Bindings.

The project should use RubyOSA to communicate with an existing, scriptable application.

The project should be implemented entirely in Ruby.

Our RSS reader will read and parse RSS feeds using the FeedTools gem. The application will use both Core Data and Bindings extensively. In part 2, we will send enclosures to an iTunes playlist using RubyOSA. And, except for a single Objective-C class, we will only write Ruby code.

3.859 out of 4 isn't bad.

Installing the Gems

First, a quick word of warning. Don't update RubyGems or any of your libraries just yet. As we will see, this may complicate things. Nothing we can't fix, but you might want to avoid problems when you can.

RubyGems is a powerful package manager for Ruby libraries. It is also a complex, command line tool. A full explanation is beyond the scope of this article, but the table below should get you started. For more information than you could ever possibly want, check out the RubyGem manuals at

Note: many of these commands (especially install, update and uninstall) require root access. You typically launch them as sudo commands.

Also, I deliberately left one command off the list: gem update –system. This updates the RubyGem system itself. Unfortunately, unlike the other gem updates, this actually changes your system files, and these changes are not easily undone.

I strongly recommend leaving this command alone. Let Apple manage the RubyGems system. As I'm writing this, they just updated RubyGems as part of the 10.5.2 release, so it should stay reasonably current. Modify the gems as much as you want, but leave the system alone.

Most of the time, you will use simple install and update commands; however, the others can come in handy when things go wrong. Updates do not always proceed as smoothly as I would like. Sometimes they leave a gem or two behaving badly. I often find that uninstalling and reinstalling the offending gem (and possibly its dependencies) sorts things out.

Now that we understand the basics of RubyGems, our first step should be the simplest. We just need to install our project's RubyGem libraries. In theory, this should only require typing the following command, entering your password when prompted.

sudo gem install feedtools

Unfortunately, life is never this easy. The FeedTools library contains the deprecated ruby-gem command. As long as you're still running the version of RubyGems that came with Leopard, you shouldn't have any problems. The library just logs a few warnings to the console. However, newer versions of RubyGems no longer recognize this command. Bottom line, if you've updated to 10.5.2, you have the new version of RubyGems, and the FeedTools library will crash.

To fix this, you simply need to edit feed_tools.rb. You can find this file at /Library/Ruby/Gems/1.8/gems/feedtools-0.2.26/lib/feed_tools.rb. Globally replace "require-gem" with "gem".

Creating the Project

Now, we can create our project. Open Xcode, and from the File menu select New Project.... In the Assistant window, scroll down and select Cocoa-Ruby Core Data Application. Click Next.

Creating our Cocoa-Ruby Core Data Application

Enter RubyRSS for the project name. Set the project directory to whatever you wish. Click Next again. Abracadabra. . .project created!

But, lets take a quick look at what Xcode has done.

MainMenu.nib and RubyRSS_DataModel.xcdatamodel are standard files for any Core Data application. The first defines our user interface. The second defines our data model. We will take a closer look at both in just a second.

Open main.m. This is the starting point for our application. As you can see, a RubyCocoa application's main simply imports the RubyCocoa runtime, then launches rb_main.rb using the RBApplicationMain() function.


#import <Cocoa/Cocoa.h>
#import <RubyCocoa/RBRuntime.h>
int main(int argc, const char *argv[])
   return RBApplicationMain("rb_main.rb", argc, argv);

Our Ruby code really starts with rb_main.rb. The default implementation loads the RubyCocoa library, locates the application's resource path, then loads any files ending with .rb using Ruby's require() method. This creates all our Ruby classes. Once finished, rb_main.rb calls NSApplicationMain(), which initializes and runs the Cocoa application.


require 'osx/cocoa'
def rb_main_init
   path = OSX::NSBundle.mainBundle.resourcePath.fileSystemRepresentation
   rbfiles = Dir.entries(path).select {|x| /\.rb\z/ =~ x}
   rbfiles -= [ File.basename(__FILE__) ]
   rbfiles.each do |path|
      require( File.basename(path) )
if $0 == __FILE__ then
   OSX.NSApplicationMain(0, nil)

With our Ruby classes now defined, we can access them from Objective-C. Unfortunately, we cannot directly import Ruby classes into Objective-C files; however, we can indirectly access the classes by name. While we won't do this in our application, the following code snippet shows the basic technique. It creates a MyRubyClass object defined in a MyRubyClass.rb file. It then calls the object's mySampleMethodCall().

Accessing Ruby from Objective-C

Class myRubyClass = NSClassFromString(@"MyRubyClass");
id ruby = [[myRubyClass alloc] init];
[ruby mySampleMethodCall]

Notice how RubyCocoa seamlessly translates objects between Ruby and Objective-C. Usually, you won't need to worry, things just work.

Sooner or later, however, you will rub up against one of the rougher edges. For example, RubyCocoa converts Ruby objects into Objective-C equivalents when possible. This means you can pass Ruby Strings to Objective-C methods. RubyCocoa will automatically convert them into NSStrings.

However, the reverse is not true. RubyCocoa will place a Ruby wrapper around Objective-C classes, and will sometimes add convenience methods (like adding each() to NSString, NSArray and NSDictionary), but it does not convert the classes.

So, if RubyCocoa calls an Objective-C method that returns a string, the Ruby code will get an NSString, not a Ruby String. A quick call to to_s() fixes this, but it can cause bugs if you're not careful. Also, Ruby and Objective-C sometimes have very different ideas about booleans. We'll take a closer look at that little wrinkle later.

My advice, ignore object conversions until they cause problems. This is best dealt with on a case-by-case basis.

Finally RubyRSSAppDelegate.rb acts as a Ruby-implemented delegate for our application. Feel free to poke around this file. However, you'll find the most interesting bits at the very beginning. This class not only imports the Core Data framework, it also subclasses NSObject. This just demonstrates how easily Ruby and Objective-C code can mix.


This Ruby code imports a Cocoa framework, then subclasses an Objective-C object.

require 'osx/cocoa'

OSX.require_framework 'CoreData'

class AppDelegate < OSX::NSObject


Defining the Model

Building a Core Data model is beyond the scope of this article. For more information, take a look at the Core Data Tutorial video (,/a>) or Apple's article on creating managed object models with Xcode (

For simplicity's sake, lets import our model from the online source code for this article. First, download the source code from Delete RubyRSS.xcdatamodel from your project. Select Also Move to Trash when prompted. Then, select Project... Add to Project... In the file dialog, select RubyRSS.xcdatamodel from the source code's folder. Press Add. In the next dialog, make sure Copy items into designation group's folder (if needed) is selected. Click Add again.

Now, open RubyRSS.xcdatamodel, and let's poke around inside. RubyRSS uses a simple model with only three Entities: Feed, Post and Enclosure.

RubyRSS's Data Model

The Feed entities represent our RSS subscriptions. Feed has three attributes: name, url and count. It also has a too-many relationship with Post.

Post has two attributes: title and text. Post also has two relationships: one points back to Feed, while a to-many relationship points to Enclosure. So far, so good–this isn't exactly rocket science.

Finally, Enclosure has two attributes: url and isAudio. It also has a single relationship with Post.

The attributes have straightforward data types. I've listed the details below, but nothing should come as a surprise. Also, if you look carefully at the model, you will see that I've placed some restrictions on the data. In general, I recommend making your data as restrictive as possible; however, we don't need data validation for this tutorial, so I'll let you explore it on your own.

Enclosure and Post are both NSManagedObjects. However, Feed's count attribute needs a bit of special attention. Count represents the number of posts associated with this feed. To get this behavior, we will need to subclass NSManagedObjects and override the count() accessor.

Now, as I said earlier, I hoped to implement everything using Ruby. This will be the one exception. Trying to write this in Ruby just creates problems; the default AppDelegate implementation automatically creates Key Value Coding (KVC) wrappers for any attributes declared in the NSManagedObjectModel. Since this occurs after our classes have loaded, our custom count() method gets clobbered.

We could fix this, but it's easier to write ManagedFeed in Objective-C, and I'm all about the pragmatic.


This is the header file for our ManagedFeed class.
#import <Cocoa/Cocoa.h>
@interface ManagedFeed : NSManagedObject {


This is the implementation of our ManagedFeed class.

#import "ManagedFeed.h"
@implementation ManagedFeed
+(NSSet*) keyPathsForValuesAffectingCount {
   NSSet *set = [super keyPathsForValuesAffectingValueForKey:@"posts"];
   return [set setByAddingObject:@"posts"];
-(int)count {
   id posts = [self valueForKey:@"posts"];
   NSArray * all = [posts allObjects];
   return [all count];

You can find a detailed description of the keyPathsForValuesAffecting<key> method in the NSKeyValueObserving protocol reference. Essentially, this method describes the dependencies for a given key. KVO uses this to determine if and when the key may have changed. In our code, count could change whenever the value of the post key changes. We could specify this by just returning a set that contains @"post".

However, our implementation is a little more complicated. Apple recommends requesting an initial set of keys from the super class, then appending your own key paths to that set. In this tutorial, the call to the super class will always returns an empty set. However, this implementation protects us from future changes.

In the count method, we return the number of posts associated with this feed. We get a copy of the posts relationship using KVC . Then we extract an NSArray containing these posts. Finally, we return the number of objects in our NSArray.

Building the Controllers

Apple now recommends building your controllers before designing your interface. You can still create controller objects within Interface Builder and then export them back to Xcode. You can even export your controllers in Ruby and Python; however, I could not get the resulting code to run. Best to follow their advice and just write the controllers yourself.

Just like the standard Objective-C versions, our Ruby controllers combine outlets, actions and possibly a few helper functions. Outlets represent the UI elements that we will need to programmatically interact with. Actions represent UI-driven events.

Fortunately, RubyCocoa provides an attr_accessor-like method for defining outlets. For those not familiar with attr_accessor, it takes any number of symbols, and creates an instance variable for each one. Attr_accessor also creates the getter method <symbol>() and the setter method <symbol>=(). For example, attr_accessor :name creates @name, name() and name=().

Similarly, ib_outlet takes a comma-separated list of symbols. It converts each symbol into an instance variable with the same name. A corresponding outlet will also appear in Interface Builder.

Note: you should avoid using attr_accessor in your RubyCocoa code. Unfortunately, attr_accessor does not create KVC compliant variables, so we cannot connect to them using Bindings. The getter works fine, but Cocoa expects a set<Symbol>() setter (setName() in our example).

Fortunately, RubyCocoa provides kvc_accessor. Kvc_accessor works identically to attr_accessor, but creates KVC compliant methods.

RubyCocoa also simplifies declaring KVC dependencies. The kvc_depends_on() method takes two parameters: an array of symbols representing the dependencies, and a single symbol representing the calculated attribute.

Basically, this method replaces Objective-C's keyPathsForValuesAffecting<key>(). Take a look at our ManagedFeed.m file again. The keyPathsForValuesAffectingCount method defines count's dependency upon posts. In Ruby, we could replace that method with a single line:

kvc_depends_on([:posts], :count)

Finally, RubyCocoa elegantly handles actions. Simply define a method with a single parameter, usually named sender. After the method, add a call to ib_action() passing in the method's name as a symbol.

Sample RubyCocoa Action

def myAction
ib_action :myAction

Now, the Rubyists out there have undoubtedly noticed that the RubyCocoa formatting looks a bit odd. Most of this creeps in when we translate Objective-C syntax into Ruby.

Objective-C's syntax uses both named arguments and colons–neither of which translates nicely. Therefore, when referring to an Objective-C method, concatenate all the pieces of its signature, and replace the colons with underscores.

[canvas print: text withFontColor: red];


canvas.print_withFontColor_(text, red)

As a bit of syntactic sugar, RubyCocoa allows you to drop the final underscore. So, print_withFontColor_() becomes print_withFontColor(). Note: the Ruby and Python Programming Topics for Mac OS X article claims that this option is disabled by default. This is not true. In most cases, you can use the two variants interchangeably. The exceptions, however, can cause real pain.

When Objective-C calls a Ruby method that overrides an Objective-C method (Ah, yes. She knows that I know that she knows that I know. . . .), RubyCocoa looks for the method signature without the trailing underscore. So, just to prevent possible problems, I recommend universally dropping the last underscore.

For consistency, I've tried to use camel case for actions (likeThis). Pure-ruby helper functions have the more-traditional underscore names (like_this).

OK, enough babbling. Let's look at the code. We will have two windows in our UI, the main window, and a dialog for adding new feeds Let's create a controller for each: MainController.rb and AddFeedController.rb respectively.


This class acts as the controller for our main window. It responds to all the main window's actions, and makes changes to the data model. It will also coordinate with both the FeedTools and RubyOSA libraries when necessary.

require 'osx/cocoa'
# Controller for the Main window.
class MainController < OSX::NSObject
   ib_outlet :feeds, :posts, :enclosures, :web_view, :posts_table, 
      :enclosures_table, :progress, :app_delegate
   # accessor for the current Feed collection.
   def feeds
      return @feeds.arrangedObjects
   # Add remaining methods here

Here, we're building a subclass of NSObject. We start by declaring a slew of outlets for Interface Builder. The feeds() accessor returns an array containing all our Feed entities. This represents all currently subscribed feeds.

The next two methods override NSObject methods. The Cocoa framework will automatically call these.

NSObject Methods

# Initializes the Main Window after it is loaded from the NIB.
def awakeFromNib
   @posts.addObserver_forKeyPath_options_context(self, "selection", 
      0, nil)
# This listener method will be called whenever the Post Table's selection 
# changes. It updates the HTML in the web view.
def observeValueForKeyPath_ofObject_change_context( key_path, object, 
   change, context)
   set_html if @posts.isEqual(object)

The framework calls our awakeFromNib() method after all objects have been loaded from the nib file, and once all outlets are set. We can use this method to perform any additional initialization. In our case, we make the NSProgressIndicator invisible when not in use. We also force our controller to listen for any changes to the @posts selection.

The framework now calls observeValueForKeyPath_ofObject_change_context() whenever @posts's selection changes. We simply verify that we're receiving an update from @posts, then call the set_html() helper method.

Note: As I mentioned earlier, you must drop the final underscore from this method's name. Otherwise, Key Value Observing (KVO) cannot find our implementation.

Next, we declare two actions: sendToItunesAction() and refreshFeedsAction(). Currently, they just print a message to the console.


# This action sends the currently selected Enclosure to iTunes.

def sendToItunesAction(sender)
   puts "Send to iTunes"
ib_action :sendToItunesAction
# This action refreshes all the feeds.   
def refreshFeedsAction(sender)
      puts "Refresh Feeds"
ib_action :refreshFeedsAction

Add_feed() adds a new Feed entity to the managed object context. It then fills in the feeds attributes. Notice that it leaves the posts relationship blank. We don't have any posts yet.


# Adds a new feed to the Managed Object Context
def add_feed(name, url) 
   moc =  @app_delegate.managedObjectContext
   new_feed = OSX::NSEntityDescription\
   new_feed.setValue_forKey(name, "name")
   new_feed.setValue_forKey(url, "url")

Finally, set_html() gets the text from our currently selected post. We then display this text in our web view.


# Helper Function: Updates the HTML displayed by the Web View to the text 
# of the currently selected post.
def set_html
   index = @posts_table.selectedRow
   frame = @web_view.mainFrame
   # If nothing is selected, just return.
   if index < 0 then
      frame.loadHTMLString_baseURL("", nil)
      post = @posts.arrangedObjects[index]
      frame.loadHTMLString_baseURL(post.text, nil)   

Our second controller is even simpler. This controller has only two outlets, plus two KVC-compliant properties, and a third virtual property.

The sheet outlet provides access to the Add Feed dialog sheet, while the window_controller provides a link back to our main controller.

The name and url properties hold (not surprisingly) the name and URL of the new feed.

Finally, the virtual property, valid_feed, returns true if the feed has a valid name and URL. Obviously, valid_feed depends upon the name and url properties. Key Value Observing will call our valid-feed accessor whenever either of the dependent variables changes.


require 'osx/cocoa'
# AddFeedController acts as the controller for the Add Feed sheet.
class AddFeedController < OSX::NSObject
   ib_outlet :sheet, :window_controller
   kvc_accessor :name, :url
   kvc_depends_on([:name, :url], :valid_feed)
   # Add methods here

The open_dialog() method opens the Add Feed dialog. We declare this as an action, so that we can link it to a button on the main window.


# This action opens the Add Feed sheet.
def open_dialog(sender)
      didEndSelector_contextInfo(   @sheet, 
ib_action :open_dialog

Next, we add our add_feed() action. We will link this action to the Add button on the Add Feed dialog sheet. This method simply converts the feed name and URL into Ruby Strings, then delegates back to the main window controller's add_feed() method. Finally, it closes the Add Feed sheet.


# The add_feed action grabs the name and url from the Add Feed sheet, 
# adds the new feed to the Managed Object Context, then closes the sheet.
def add_feed(sender)
   feed_name = @name.to_s
   feed_url = @url.to_s
   @window_controller.add_feed(feed_name, feed_url)
ib_action :add_feed

The cancel() action simply closes the Add Feed dialog sheet. We will link this action to the Cancel button on the Add Feed sheet.


# The cancel action closes the sheet without adding a new feed.
def cancel(sender)      
ib_action :cancel

The valid_feed() method uses Ruby's regular expressions to filter out invalid entries. Basically, the feed name must contain at least one non-whitespace character, while the URL must start with "feed://", then contain one or more characters, a period, and end with one or more characters. The URL cannot have any white space.

Note: While Ruby has explicit true and false values, it also treats all nil values as false, and all non-nil values as true. This means, the result of ANDing together two regular expressions is either nil or the String matched by the second regular expression. While Ruby will correctly interpret this as a boolean value, when we pass it to the Cocoa framework, we get the following exception:


SetValue_forKey: OSX::OCException: 
NSInternalInconsistencyException - 
  Cannot create BOOL from object <RBObject: 0x1437bdd0> 
  of class RBObject

To prevent this, we explicitly convert our result to a boolean value.


# valid_feed returns true if the Add Feed sheet's current name and url 
# values are valid. This method can be monitored using KVO. 
def valid_feed
   name = @name.to_s
   url = @url.to_s
   result = name.match('\S+') && url.match('^feed://\S+\.\S+$')
   # explicitly convert to booleans.
   return ! result.nil?

Finally, the private helper function, close_dialog() clears the Text Fields and closes the dialog.


# Helper Function: close_dialog clears the Add Feed's 
# text boxes, then closes the sheet.
def close_dialog

Building the User Interface

Building the user interface is also beyond the scope of this article. Simply copy MainMenu.nib from the online source code.

Our user interface consists of the main window and the Add Feed panel. We have three array controllers. The first contains all of our Feed entities. The second contains all Post entities associated with the currently selected Feed. The third contains all Enclosure entities associated with our currently selected Post. Bindings automatically maintain these relationships, requiring no code on our part.

Finally, we have the Add Feed and the Main controllers defined in the previous section.

RubyRSS's Arrays and Controllers

The Main window consists of three Table Views. The first contains the names and post counts from the Feeds array. The second displays titles from the Posts array. The final table contains URLs from the Enclosures array. Again, we set all of these values using Bindings. Of these, only the feed names are editable.

The Main window also has a Web View. This contains the text for the currently selected post; however, unlike the Table Views, we cannot set the Web View's content using Bindings. Instead, we actually have to write code.

The good news is, you've already written this code. Look back at our main controller. Remember, how it receives notifications whenever the posts' selection changes? It then fires the set_html() helper function. That's the code we need. We use KVO to automatically synchronize our web view with the current selection. Basically, we're recreating the code that Bindings normally gives us for free.

Finally, our main window has four Buttons: one adds a new feed, one deletes the selected feed, one sends the selected enclosure to iTunes, and the last one refreshes all our feeds. Since some of these operations can take a long time, we also have a Progress Indicator.

RubyRSS's Main Window

Even simpler, the Add Feed panel has two Text Fields: one for the Feed's name and one for the URL. Each Text Field has a corresponding Label. Finally, we have an Add Button and a Cancel Button.

Add Feed Window

The connections between our UI elements and the controllers' outlets and actions should seem straightforward enough. I won't go into the details here, but I encourage you to open up the nib in Interface Builder and get a feel for the wiring.

One last quick step. Since our user interface uses a Web View, we need to add the WebKit.framework to our project. Right click on the Frameworks folder in the Groups & Files tree. Select Add... Existing Frameworks.... In the File dialog, find and select the WebKit.framework and select Add. In the next panel, just select Add again.

You can now compile and launch the application. Of course, it won't do much yet. We can add new feeds, but we cannot actually read or parse them. All the basic RubyCocoa code works, but we still need to add support for FeedTools and RubyOSA.

Parsing the Feeds

The FeedTools library provides code for parsing, generating and auto discovery of RSS, atom and cdf feeds. We're only using a fraction of its abilities. If you want to know more, check out the web page for FeedTools and its sister project FeedUpdater (

Let's make a new class to handle the interactions between FeedTools and our data model. Create a new Ruby class named FeedReader.rb.


require 'rubygems'
require 'feed_tools'
require 'osx/cocoa'
OSX.require_framework 'CoreData'
# FeedReader class uses feed_tools to download and parse all the feeds, 
# then adds new posts and enclosures to the Managed Object Context.
class FeedReader
   # insert methods here

FeedReader starts by loading the required libraries. Obviously we need FeedTools and RubyCocoa. The RubyGems library lets us access any gem-installed libraries; therefore, we need to load RubyGems before loading FeedTools. Most interestingly, the OSX.require_framework method lets us load Cocoa frameworks. In this case, FeedReader needs Core Data.


# Default Constructor
def initialize(main_controller, moc)
   @main_controller = main_controller
   @moc = moc

Initialize() allows us to construct new FeedReader objects. It takes two arguments: a reference to the main window controller, and a reference to our managed object context.


# Gets the current list of feeds. Downloads all Feeds and adds new Posts 
# and Enclosures to the Managed Object Context.
def refresh
   feed_entries = @main_controller.feeds
   feed_entries.each {|data_feed| update(data_feed)}

FeedReader only exposes a single method to the outside. Refresh() iterates over all the feeds, passing each one to the update() helper function.


# Helper Function: updates a single Feed.
def update(data_feed)
   feed =
   posts = feed.entries
   posts.each{|post| add_post(post, data_feed)}

Update() extracts the list of available posts for each feed. It iterates over the list of posts, calling add_post() for each one.


# Helper Function: adds new posts to the given feed.
def add_post(post, data_feed) 
   title = post.title
   text = post.summary
   # check to see if this already exists...
   return if post_exists?(title, text)
   # now make a new entity
   data_post = OSX::NSEntityDescription\
   data_post.setValue_forKey(title, "title")
   data_post.setValue_forKey(text, "text")
   data_post.setValue_forKey(data_feed, "feed")
   enclosures = post.enclosures
   enclosures.each do |enclosure| 
      add_enclosure(enclosure, data_post)}

Add_post() extracts the post's title and text. It then calls post_exists?(), checking if any posts in the managed object context already have a matching title and text. If the post doesn't already exist, add_post() adds a new Post Entity. It then fills in the entity's attributes and sets the feed relationship.

Since we're using bi-directional relationships, Core Data automatically adds this post to its Feed. Finally, add_post() iterates over the post's list of enclosures, calling add_enclosure() for each one.


# Helper Function: determine if a post exists with the given title and 
# text.
def post_exists?(title, text)
   request = OSX::NSFetchRequest.alloc.init
   description = OSX::NSEntityDescription\
      .entityForName_inManagedObjectContext('Post', @moc)
   predicate = OSX::NSPredicate.predicateWithFormat(
      "title like %@ AND text like %@", title, text)
   error = nil
   count = @moc.countForFetchRequest_error(request, error)
   # if we have an error, assume the post doesn't exist.
   if !error.nil?
      puts "*** Error ***"
      puts error
      return false
   count > 0

Post_exists?() builds an NSFetchRequest for all Post entities whose title and text mach the given arguments. We then use countForFetchRequest_error_() to count the number of matching Posts. For any number greater than zero, we return true. Otherwise, we return false. Note: we log any errors, but simply assume no matches are found.


# Helper Function: adds a single Enclosure to the given Post.
def add_enclosure(enclosure, data_post)
   url = enclosure.url
   isAudio =
   new_enclosure = OSX::NSEntityDescription.\
         "Enclosure", @moc);
   new_enclosure.setValue_forKey(url, "url")
   new_enclosure.setValue_forKey(isAudio, "isAudio")
   new_enclosure.setValue_forKey(data_post, "post")

Finally, add_enclosure() adds a new Enclosure entity to the managed object context. It then fills the attributes, and sets the post relationship. Again, we have a bi-directional relationship, so Core Data automatically adds this Enclosure to the Post's enclosures relationship.

Now we just need to make our MainController aware of the FeedReader. Add the following line to MainController's awakeFromNib() method:

@feed_reader =, @app_delegate.managedObjectContext)

This creates a new FeedReader object. We just need to call @feed_reader.refresh() whenever the user presses the Refresh Feeds button. However, this operation can take a while, especially when you subscribe to a lot of feeds. We don't want our UI to freeze up. Also, we would like to let the user know that something is actually happening. So, let's have FeedReader refresh the feeds in a second thread, and turn on the progress bar.

Unfortunately, Ruby 1.8 is not thread safe. We cannot call Ruby code from a second (or third, or fourth...) Objective-C thread. However, we can use Ruby's internal threads. Ruby uses a green threading model. Basically, as far as the hardware knows, Ruby runs in a single thread, but the Ruby interpreter can time slice between several green threads.

Ruby threads have some advantages and some disadvantages over processor threads. A full discussion is beyond the scope of this article, but — bottom line — they work fine for our purposes. The user interface will not freeze up while we refresh the feeds.

Simply replace refreshFeedsAction() with the following code.

new refreshFeedsAction()

# This action refreshes all the feeds.
def refreshFeedsAction(sender) do
      rescue Exception => e
         puts e.message
ib_action :refreshFeedsAction

The begin. . .rescue. . .end blocks handle any exceptions thrown in our worker thread. If an error occurs, it executes the rescue block, which logs the error and quits the application.

I'm a firm believer in failing fast. We don't want our code to lumber ahead in an unknown state. At least during development, go ahead and force the application to stop as soon as an error occurs.

That's it. Open up the RSS reader and add a few feeds. Click the Refresh Feeds button, and watch the posts roll in. Quit the application, and then launch it again. Core Data automatically saves all our data.

The Complete RubyRSS

This almost looks like a real application. Almost. It still needs a lot of work. For example, the tables remain completely unsorted. Ideally, users will want to filter them as well. By default we should probably filter out any posts that the user has already read. Searching would also be nice.

From a software engineering standpoint, we're playing fast and loose with our threads here. The user can perform any number of bad actions (like quitting the application) while the worker thread is still running. We should probably address that.

But, you have to admit, we squeezed a ton of functionality out of a few hundred lines of code. Ruby + Core Data + Bindings makes a high-octane combination.

Next time, we will look at using RubyOSA to send enclosures to iTunes. We will also take a look at RubyCocoa's debugging options, and look at a few other cool tricks as well.

Rich Warren lives in Honolulu, Hawaii with his wife, Mika, daughter, Haruko, and his son, Kai. He is a software engineer, freelance writer and part time graduate student. When not playing on the beach, he is probably writing, coding or doing research on his MacBook Pro. You can reach Rich at, or check out his blog at


Community Search:
MacTech Search:

Software Updates via MacUpdate

f.lux 42.1 - Adjusts the color of your d...
f.lux makes the color of your computer's display adapt to the time of day, warm at night and like sunlight during the day. Ever notice how people texting at night have that eerie blue glow? Or wake... Read more
Spotify - Stream music, creat...
Spotify is a streaming music service that gives you on-demand access to millions of songs. Whether you like driving rock, silky R&B, or grandiose classical music, Spotify's massive catalogue puts... Read more
Vitamin-R 4.15 - Personal productivity t...
Vitamin-R creates the optimal conditions for your brain to work at its best by structuring your work into short bursts of distraction-free, highly focused activity alternating with opportunities for... Read more
OfficeTime 2.0.628 - Easy time and expen...
OfficeTime is time and expense tracking that is easy, elegant and focused. Other time keepers are clumsy or oversimplified. OfficeTime balances features and ease of use, allowing you to easily track... Read more
Slack 4.28.182 - Collaborative communica...
Slack brings team communication and collaboration into one place so you can get more work done, whether you belong to a large enterprise or a small business. Check off your to-do list and move your... Read more
DEVONthink Pro 3.8.6 - Knowledge base, i...
DEVONthink is DEVONtechnologies' document and information management solution. It supports a large variety of file formats and stores them in a database enhanced by artificial intelligence (AI). Many... Read more
FileMaker Pro 19.5.4 - Quickly build cus...
FileMaker Pro is the tool you use to create a custom app. You also use FileMaker Pro to access your app on a computer. Start by importing data from a spreadsheet or using a built-in Starter app to... Read more
Backblaze - Online backup serv...
Backblaze is an online backup service designed from the ground-up for the Mac. With unlimited storage available for $6 per month, as well as a free 15-day trial, peace of mind is within reach with... Read more
Day One 7.16 - Maintain a daily journal.
Day One is an easy, great-looking way to use a journal / diary / text-logging application. Day One is well designed and extremely focused to encourage you to write more through quick Menu Bar entry,... Read more
Garmin Express - Manage your Ga...
Garmin Express is your essential tool for managing your Garmin devices. Update maps, golf courses and device software. You can even register your device. Update maps Update software Register your... Read more

Latest Forum Discussions

See All

We’re Digging ‘Shovel Knight Dig’ – The...
We spend the bulk of this week’s podcast talking about the new iPhone 14. Specifically, the iPhone 14 Pro Max which both Eli and myself picked up. The consensus seems to be: They’re great! They’re iPhones! We do lay down our hot takes on all the new... | Read more »
TouchArcade Game of the Week: ‘Loose Noz...
There aren’t a lot of stories like that of the development of Loose Nozzles, and of those games that do have an interesting development story, even fewer are actually decent games to play. Loose Nozzles nails both, though. The way it was created is... | Read more »
SwitchArcade Round-Up: ‘Shovel Knight Di...
Hello gentle readers, and welcome to the SwitchArcade Round-Up for September 23rd, 2022. In today’s article, we’ve got the rest of this week’s releases to look at. There are actually a few big games today, including the hot-hot-hot Shovel Knight Dig... | Read more »
‘Gubbins’ is a Way Too Adorable Word Gam...
There are games whose art style, sounds, and overall vibe just make me smile ear to ear. Games like Hidden Folks, Krispee Street, or Tiny Wings. There’s just something so cool about being able to literally feel the heart that goes into a game. Now... | Read more »
Based on the Baking Reality Show, ‘Naile...
Fans of Netflix’s reality baking show Nailed It! have a new holiday-themed season to look forward to next month when Nailed It! Halloween launches on October 5th, but the fun doesn’t stop there because the show is also arriving as a mobile game the... | Read more »
Cookie Run: Kingdom announces collaborat...
In news sure to excite fans of biscuits or K-Pop music, the Korean sensations BTS have teamed up with Cookie Run: Kingdom for a series of events. After some warm-up episodes, the collaboration will culminate in a BTS in-game concert, so if anyone'... | Read more »
‘Shovel Knight Dig’ From Nitrome and Yac...
Shovel Knight Dig () from Nitrome and Yacht Club Games is this week’s new Apple Arcade release. It is definitely one of my favorite additions to the service ever, and a fantastic game overall. I played it a few hours ago when it started rolling out... | Read more »
SwitchArcade Round-Up: ‘Mario Strikers’...
Hello gentle readers, and welcome to the SwitchArcade Round-Up for September 22nd, 2022. Hunh, lots of twos in the date today. Nifty. As those who read yesterday’s article may remember, I got a vaccine shot about twenty four hours ago and it is... | Read more »
Rogue-Like Platformer ‘Tallowmere 2’ Lau...
The original Tallowmere from developer Chris McFarland launched on mobile way back in 2015, and to be honest it did not leave a good first impression with me. For lack of a better term, it just seemed… janky, and right from the start the game sort... | Read more »
Alchemy Stars newest event launches and...
Alchemy Stars has introduced its latest event, entitled Farewell, My Wonderland, bringing with it new characters and a bevvy of rewards. The event will reportedly focus on the underlying message that even after tragic events there is still light,... | Read more »

Price Scanner via

Use our exclusive Apple Price Trackers to fin...
Our Apple award-winning price trackers are the best place to look for the lowest prices and latest sales on all the latest Apple gear this season. Scan our price trackers for the latest information... Read more
New promo at Verizon: Get Apple Watch Series...
Purchase a new iPhone 14 at Verizon, and get an Apple Watch Series 8 for as low as $5 per month. $120 in promo credits for the Watch are spread over a 36 month term, reducing the price of the Watch... Read more
Visible drops prices on Apple iPhone 13 model...
Verizon’s low-cost wireless cell service, Visible has dropped prices on iPhone 13 models to new low prices starting at $599: – iPhone 13 Pro Max: starting at $980 + free $200 gift card – iPhone 13... Read more
Back in stock! 14″ MacBook Pros with Apple M1...
Amazon has restocked 14″ MacBook Pros M1 Pro CPUs for $400 off MSRP, starting at only $1599. Shipping is free. Be sure to make your purchase from Amazon rather than a third-party seller. Their prices... Read more
This is the final week to take advantage of A...
Apple’s Back to School promotion for 2022 ends on September 26, 2022. As part of this promotion, Apple will include a free $150 Apple Gift Card with the purchase of any MacBook Air, MacBook Pro, or... Read more
Mac Studio with M1 Max CPU back in stock toda...
Apple has the base standard-configuration Mac Studio available again in their Certified Refurbished section for $1799, and it’s in stock today. Each Mac Studio comes with Apple’s one-year warranty,... Read more
Apple MagSafe iPhone battery on sale for $84,...
Amazon has Apple’s MagSafe Battery on sale for $84 today. Shipping is free. That’s $15 off Apple’s MSRP, and it’s the lowest price for one of these MagSafe batteries among the Apple retailers we... Read more
24-inch M1-powered iMacs available today at A...
Apple has a full range of 24-inch M1 iMacs available today in their Certified Refurbished store. Models are available starting at only $1099 and range up to $260 off original MSRP. Each iMac is in... Read more
Verizon offers free Apple iPhone 14 models to...
Verizon is offering a $800-$1000 discounts on Apple’s new iPhone 14 models for new and existing customers with a qualified trade-in. Price of the iPhone 14 will be spread over 36 months of payments,... Read more
Gazelle drops prices on iPhone 13 models to a...
Gazelle has a full line of discounted, refurbished, unlocked Apple iPhone 13 models now available starting at $469. iPhones are offered in Fair, Good, and Excellent conditions, and multiple colors... Read more

Jobs Board

Physician Assistant, Primary Care, *Apple*...
Physician Assistant, Primary Care, Apple Valley (1.07FTE) + Job ID: 65766 + Department: AV Primary Care + City: Apple Valley, MN + Location: HP - Apple Read more
Operations Manager - Mac/ *Apple* Engineerin...
…Responsible for the day-to-day activities relating to the engineering of Apple Macs in a complex, multi-platform environment. Demonstrates strong leadership, Read more
Lead Developer - *Apple* tvOS - Rumble (Uni...
…earnings, and positive sentiment About the role: We are looking for a Lead Apple tvOS Developer to join our application engineering team to expand our video centric Read more
Systems Administrator - *Apple* Devices / J...
…Administration **Duties and Responsibilities** + Configure and maintain the client's Apple Device Management (ADM) solution. The current solution is JAMF supporting Read more
Sr Product Manager, *Apple* TV Platforms -...
…an experienced senior product manager to drive the strategy and requirements for our Apple TV devices, acting as the champion and owner of the holistic experience in Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.