Generating Menus in OSX Apps - the Ruby way!
A few months ago I made the decision to convert my Mac app from Objective-C to RubyMotion. It's a status-bar app, which means that the primary user interface is a menu, not a window. I quickly ran into issues with the generating menus in code and got quite fed up.
I've now solved most of my problems with my new gem Drink Menu; today I want to explain my motivations and show you a working code example. Finally, I'd like to ask for your help making it even better.
Menus were made to be built in Interface Builder. If you've ever tried to code one, you'll understand that. But I was made to work at the command-line in Vim. For me visual tools like IB are for rapid prototyping, but when I'm ready to commit I want to manage the UI in code, provided I can do it in a reasonably sane way.
For window-based UIs we have Teacup, a RubyMotion library that enables separating window creation from styling/layout. If you haven't seen it, I highly recommend taking a look, it's great for extracting those properties you have to set over and over again (ever tried to create a basic label?)
For menu-based UIs, we have this. That's the code RubyMotion generates for you when you create a new Mac app. It's exactly the same menu that you get in a new Cocoa app with Xcode. This is a nice wrapper around the Cocoa APIs for generating menus, and since it uses blocks it looks very Rubyish.
But the first time I needed to generate items from a collection (and update the menu when new items are added), or use an NSStatusItem it became more complicated. I really needed something a little more powerful.
Also, I've never liked the Cocoa pattern of passing a selector to execute when an action occurs. It leads to what I like to "entry-point hell"; you know, that special situation where your classes a bunch of entry-point methods, and you have to find where it's being used as a selector to find out what happens. These methods are difficult to test and makes the code a little harder to understand on a quick read (those who geek out about this stuff should read about connascence).
And there's another problem with this code. One of the things I like about teacup is that it separates the layout/style of a view from the creation of its controls, along with a DSL for specifying all of that. So I can create a generic "stylesheet" to cover different style/layout use cases, and then relate controls to those styles. This ultimately leads to much easier to maintain view code. How can we get a similar separation for our menus?
Today I'm proud to show you my initial stab at solving these problems with menu code. It's a gem called Drink Menu. It provides methods to create menu items and then gives you a nice DSL for laying out the menu items in an actual menu. Then in your
AppDelegate you can subscribe to menu item clicks with a block, rather than relying on selector matching to figure out what's going on. It also generates menus from collections, and keeps them up-to-date as you add items.
Here is the code to generate a basic main menu for an app. You can run this code by cloning the Drink Menu repo from Github and running
example=basic_main_menu platform=osx rake.
You create items using a few different factory methods, and then you refer to those items by their label when you create the menu. The block for creating a menu is a visual representation of the actual menu. To reorder items, just move them up and down in the block. To create a separator use the
___ (that's 3 underscores) method.
I'll let that sink in and follow up soon with examples for how to do status menus and bind to collections.
Call to Action
The ideas here are still young. It's ready to use in any Mac app, but I know there are use cases I'm not covering. Do you feel the same pains I do? Would you use this in your OSX RubyMotion apps today? If not, what would you change to make it more useful? Please feel free to hit me up on Twitter or via email if you have questions or feedback.