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

Adobe Creative Cloud - Access...
Adobe Creative Cloud costs $19.99/month for a single app, or $49.99/month for the entire suite. Introducing Adobe Creative Cloud desktop applications, including Adobe Photoshop CC and Illustrator CC... Read more
Default Folder X 5.1.4 - Enhances Open a...
Default Folder X attaches a toolbar to the right side of the Open and Save dialogs in any OS X-native application. The toolbar gives you fast access to various folders and commands. You just click on... Read more
Amazon Chime 4.1.5587 - Amazon-based com...
Amazon Chime is a communications service that transforms online meetings with a secure, easy-to-use application that you can trust. Amazon Chime works seamlessly across your devices so that you can... Read more
Persecond 1.0.9 - Timelapse video made e...
Persecond is the easy, fun way to create a beautiful timelapse video. Import an image sequence from any camera, trim the length of your video, adjust the speed and playback direction, and you’re done... Read more
CrossOver 16.2 - Run Windows apps on you...
CrossOver can get your Windows productivity applications and PC games up and running on your Mac quickly and easily. CrossOver runs the Windows software that you need on Mac at home, in the office,... Read more
MegaSeg 6.0.2 - Professional DJ and radi...
MegaSeg is a complete solution for pro audio/video DJ mixing, radio automation, and music scheduling with rock-solid performance and an easy-to-use design. Mix with visual waveforms and Magic... Read more
Apple iTunes 12.6 - Play Apple Music and...
Apple iTunes lets you organize and stream Apple Music, download and watch video and listen to Podcasts. It can automatically download new music, app, and book purchases across all your devices and... Read more
GraphicConverter 10.4 - $39.95
GraphicConverter is an all-purpose image-editing program that can import 200 different graphic-based formats, edit the image, and export it to any of 80 available file formats. The high-end editing... Read more
OpenEmu 2.0.5 - Open Source game-emulati...
OpenEmu is about to change the world of video game emulation, one console at a time... For the first time, the 'It just works' philosophy now extends to open source video game emulation on the Mac.... Read more
beaTunes 4.6.13 - Organize your music co...
beaTunes is a full-featured music player and organizational tool for music collections. How well organized is your music library? Are your artists always spelled the same way? Any R.E.M. vs REM?... Read more

The Elder Scrolls: Legends is now availa...
| Read more »
Ticket to Earth beginner's guide: H...
Robot Circus launched Ticket to Earth as part of the App Store's indie games event last week. If you're not quite digging the space operatics Mass Effect: Andromeda is serving up, you'll be pleased to know that there's a surprising alternative on... | Read more »
Leap to victory in Nexx Studios new plat...
You’re always a hop, skip, and a jump away from a fiery death in Temple Jump, a new platformer-cum-endless runner from Nexx Studio. It’s out now on both iOS and Android if you’re an adventurer seeking treasure in a crumbling, pixel-laden temple. | Read more »
Failbetter Games details changes coming...
Sunless Sea, Failbetter Games' dark and gloomy sea explorer, sets sail for the iPad tomorrow. Ahead of the game's launch, Failbetter took to Twitter to discuss what will be different in the mobile version of the game. Many of the changes make... | Read more »
Splish, splash! The Pokémon GO Water Fes...
Niantic is back with a new festival for dedicated Pokémon GO collectors. The Water Festival officially kicks off today at 1 P.M. PDT and runs through March 29. Magikarp, Squirtle, Totodile, and their assorted evolved forms will be appearing at... | Read more »
Death Road to Canada (Games)
Death Road to Canada 1.0 Device: iOS Universal Category: Games Price: $7.99, Version: 1.0 (iTunes) Description: Get it now at the low launch price! Price will go up a dollar every major update. Update news at the bottom of this... | Read more »
Bean's Quest Beginner's Guide:...
Bean's Quest is a new take on both the classic platformer and the endless runner, and it's free on the App Store for the time being. Instead of running constantly, you can't stop jumping. That adds a surprising new level of challenge to the game... | Read more »
How to rake in the cash in Bit City
Our last Bit City guide covered the basics. Now it's time to get into some of the more advanced techniques. In the later cities, cash flow becomes much more difficult, so you'll want to develop some strategies if you want to complete each level.... | Read more »
PixelTerra (Games)
PixelTerra 1.1.1 Device: iOS Universal Category: Games Price: $.99, Version: 1.1.1 (iTunes) Description: The world of PixelTerra is quite dangerous so you need to build a shelter, find some food supply and get ready to protect... | Read more »
Tokaido™ (Games)
Tokaido™ 1.0 Device: iOS Universal Category: Games Price: $6.99, Version: 1.0 (iTunes) Description: Discover the digital adaptation of Tokaido, the boardgame phenomenon that has already sold more than 250,000 copies worldwide, and... | Read more »

Price Scanner via

SSD Speeder RAM Disk SSD Life Extender App Fo...
Fehraltorf, Switzerland based B-Eng has announced they are making their SSD Speeder app for macOS publicly available for purchase on their website. SSD Speeder is a RAM disk utility that prevents... Read more
iPhone Scores Highest Overall in Smartphone D...
Customer satisfaction is much higher among smartphone owners who use their device to operate other connected home services such as smart thermostats and smart appliances, according to the J.D. Power... Read more
Swipe CRM Free Photo-Centric CRM Sales DEal C...
Swipe CRM LLC has introduced Swipe CRM: Visual Sales 1.0 for iPad, an app for creating, managing, and sharing visually stunning sales deals. Swipe CRM is targeted to small-and-medium creative... Read more
13-inch 2.0GHz Apple MacBook Pros on sale for...
B&H has the non-Touch Bar 13″ 2.0GHz MacBook Pros in stock today and on sale for $150 off MSRP. Shipping is free, and B&H charges NY sales tax only: - 13″ 2.0GHz MacBook Pro Space Gray (... Read more
15-inch Touch Bar MacBook Pros on sale for up...
B&H Photo has the new 2016 15″ Apple Touch Bar MacBook Pros in stock today and on sale for up to $150 off MSRP. Shipping is free, and B&H charges NY sales tax only: - 15″ 2.7GHz Touch Bar... Read more
Apple’s iPhone 6s Tops Best-Selling Smartphon...
In terms of shipments, the iPhone 6s from Apple bested all competitors for sales in 2016, according to new analysis from IHS Markit, a world leader in critical information, analytics and solutions.... Read more
Logitech Rugged Combo Protective iPad Case an...
Logitech has announced its Logitech Rugged Combo, Logitech Rugged Case, and Logitech Add-on Keyboard for Rugged Case for Apple’s new, more affordable $329 9.7-inch iPad, a complete solution designed... Read more
T-Mobile To Offer iPhone 7 and iPhone 7 Plus...
T-Mobile has announced it will offer iPhone 7 and iPhone 7 Plus (PRODUCT)RED Special Edition in a vibrant red aluminum finish. The introduction of this special edition iPhone celebrates Apple’s 10... Read more
9-inch 128GB iPad Pros on sale for $50-$70 of...
B&H Photo has 9.7″ 128GB Apple WiFi iPad Pros on sale for up to $70 off MSRP, each including free shipping. B&H charges sales tax in NY only: - 9″ Space Gray 128GB WiFi iPad Pro: $649 $50... Read more
27-inch iMacs on sale for up to $200 off MSRP...
B&H Photo has 27″ Apple iMacs on sale for up to $200 off MSRP, each including free shipping plus NY sales tax only: - 27″ 3.3GHz iMac 5K: $2099 $200 off MSRP - 27″ 3.2GHz/1TB Fusion iMac 5K: $... Read more

Jobs Board

*Apple* macOS Systems Integration Administra...
…most exceptional support available in the industry. SCI is seeking an Junior Apple macOS systems integration administrator that will be responsible for providing Read more
*Apple* Retail - Multiple Positions - Apple,...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
Director, *Apple* Program - ERP & Cloud...
Leading Pharmaceutical / Biotech Company is announcing a Director, Apple Program - ERP & Cloud Platform Architecture Lead opportunity in Northbrook, IL. Purpose & Read more
Fulltime aan de slag als shopmanager in een h...
Ben jij helemaal gek van Apple -producten en vind je het helemaal super om fulltime shopmanager te zijn in een jonge en hippe elektronicazaak? Wil jij werken in Read more
Starte Dein Karriere-Abenteuer in den Hauptst...
…mehrsprachigen Teams betreust Du Kunden von bekannten globale Marken wie Apple , Mercedes, Facebook, Expedia, und vielen anderen! Funktion Du wolltest schon Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.