27 January 2008

RubyCocoa Rocks

My infatuation with RubyCocoa continues. I've been working on a small app for the Building Web Apps folks. I'd originally been tasked with a feature that was to be done 100% within the web app. However, some of our requirements changed, and the workflow was not efficient enough. So, essentially what we moved forward on was a desktop Mac application that could interface with NetNewsWire, as well as the Building Web Apps site. The desktop app gives us a faster way to add data into the system - rapidly processing a ton of content and injecting what is desired into the web app, yet without getting slowed down by a web app interface. AJAX or Flex, or some other web UI tech wouldn't make it any faster in this particular situation.

Thus, I pursued building a Cocoa app, but this time using RubyCocoa. I've written Objective-C apps before, and spend the bulk of my time in Ruby, but this was my first opportunity to use RubyCocoa. The combination, much like JRuby, Jython and other hybrid systems, gives you "the best of both worlds." There are cons of course (slower, a few Cocoa things you can't do, debugging is harder, etc.), but for the most part, it's really nice.

For me, the infatuation stems from letting me use the aspects of each system that I am either more efficient with, or that are easier for a particular piece of functionality, all yielding a faster, and better end result. I can iterate on the app more quickly, and get a solution to BWA faster. And, one step further in the case of RubyCocoa: having the native OS integration abilities at hand.

What's been great is the ability to use Ruby's more effective (for me at least) string processing, XML processing, and networking features; create a native Mac application - using any cool Cocoa/native features; and support for AppleScript, which was critical for this particular application. This latter feature ruled out using something like Adobe Air.

This evening I setup the second use of AppleScript/Apple Events in this app: registering a custom URL protocol for the app. Applications like Pukka and Mailplane do this. What for? Well, in this case, it allows me to create a web browser bookmarklet, that can send data from the browser to our app. It also means that it works in basically any browser on the Mac (as opposed to AppleScript not working for Firefox). Further, it allows a simple "push a button" in the browser to send the data over to the app, as opposed to having to switch to the app, pick a menu item to pull the data, or horror of horrors, copy-paste.

How do you do this? This article is short and covers how to do it in Objective-C. It's just as easy in RubyCocoa: You need to add an entry into your Info.plist file to specify the name of your URL protocol as described in the article. Then, you need to register your app as a handler for that protocol:

self, :getUrl_withReplyEvent, fourcharcode('GURL'), fourcharcode('GURL'))

The fourcharcode method is my way of translating four character codes for use in Ruby. I discussed this in more detail in my last post about RubyCocoa, but here's the actual method for your pleasure:

def fourcharcode(character_code)

Ok, so now that you've registered your app to handle its custom URL protocol, you will get an Apple Event sent to you with the URL whenever one is opened. This is handled (as per the parameter in the registration function above), by the getUrl_withReplyEvent method:

def getUrl_withReplyEvent(event, reply)
url = event.paramDescriptorForKeyword(fourcharcode('----')).stringValue
# url now contains the complete URL as a string
# do your processing of the URL/content...

That's it. Pretty cool eh? Handling events from NetNewsWire is almost identical (register for them, write a handler function).

And, one more great thing I could integrate: Sparkle. Sparkle is a superb Cocoa library that does automatic application updates. It checks the web for a newer version of your app, downloads it, and installs it. Integrating it is simple, and in fact, depending on your needs, you don't have to write a single line of code. The only code I wrote for it was actually a Rake task to build the appcast and upload it to the server. Slick.

All this could only be done as a native Mac app, which means Cocoa. But, as said above, doing it with RubyCocoa gives me access to all these abilities, yet, I can do all the more heavy string and XML processing I need to do using Ruby, which is much more effective for me. Also, the web services calls and code is a lot easier for me to do in Ruby than Cocoa.

Furthermore, this is plain fun! Unlike some, I feel desktop apps still have a place, but love webapps at the same time. With RubyCocoa I can build super cool Mac apps, but do so in a language I'm happier using, yet have the power of Cocoa available to me. For me, some of the best "applications" these days are such hybrids: a web app that does your primary data storage, and gives you access to the app from "anywhere" (i.e. anywhere you can get to a browser and net connection), but a desktop app to use most of the time for faster interaction, potentially better integration on your desktop system, and so on. It's the same reason I use Mailplane (desktop app for Gmail), or PackRat (desktop app for Backpack). I suspect it's the same reason we're seeing other solutions like Adobe Air, or Google Gears. Technology is so cool, isn't it?!


Andy Kent said...


The events code really saved me a lot of time.

Any chance of a sneaky peak at your sparkle code and the rake file you talk of?

Chris said...

Andy, glad it helped - I wish I'd had that. You'd think this would be some standard stuff they'd have in the docs, but maybe they don't expect this as mainstream, or they're still ramping up the docs.

As for Sparkle, good idea, I'll post my rake tasks in another blog post, will work on that today. There is actually NO code for Sparkle! Their implementation is great, you literally drop the thing in with Interface Builder, hook it to your menu, and off you go (check the Sparkle site for instructions).

Chris said...

Andy, I put the Rake tasks up, hope it helps.

mormon said...

Cool beans. I might try to build something in Ruby now [a MondoMouse competitor] instead of in straight Objective C. This could be fun. Also have you tried macruby? I assume it's about the same? Thanks.

Chris said...

I haven't tried MacRuby yet. The last time I checked, it wasn't far enough along. I haven't had occasion to work in RubyCocoa in the last couple months, so it's been a while. I was happy to see MacRuby being done, just adds more to the mix. If you try it, let me know what you think.

Anonymous said...

I'm curious as to *where* I can put that code in my RubyCocoa app. I created a new non-Document 'Core Data' RubyCocoa application, and while the Info.plist section did, in fact, cause OS X to launch my app when I typed 'myapp:test' in Safari's address bar... it didn't *do* anything. As far as I could tell, `getUrl_withReplyEvent` never got called (I placed `system "say '#{url}'"` in there to test exactly that).

I put all the code into rb_main.rb, and it seemed to have no effect - here's the exact code I needed:

Is the `registerMyApp` method called automatically at some point? Or do I need to call that from somewhere in the initialization myself? I tried placing a call to `registerMyApp` from the `rb_main_init` method, but that didn't help.

Oh, one small note - you need `OSX::NSAppleEventManager` instead of just `NSAppleEventManager` in your example code.

Chris said...

@elliottcable You want to call the registration function from awakeFromNib in your main controller/UI code.

Anonymous said...
This comment has been removed by a blog administrator.