How to quickly get started creating status bar apps for Mac OS X using RubyMotion Oct 18 2013

Recently here at KickCode we've been working on our first Mac OS X application, due for submission to the App Store shortly, and using RubyMotion. We'll be unveiling it shortly, but it's a fairly simple utility app that lives in the status bar. Status bar apps are quite useful, whethere it's a utility for day to day productivity tasks, or whether it's a connection or window into an online service, that provides background syncing, notifications, or other information.

New gem

The good news is, they're fairly simple to get up and running using RubyMotion! Indeed, as with a lot of work in RubyMotion, it's a lot less effort than building the equivalent in Xcode and Objective-C! And we've just made it a bit easier still, with a small gem called osx-status-bar-app-template that provides you with a new RubyMotion app template.

Simply run:

gem install osx-status-bar-app-template

And then once installed, you can go ahead and create your new RubyMotion app using the template:

motion create --template=osx-status-bar-app TestStatusBarApp

Then simply fire it up to see the basic skeleton app in action, ready for building upon:

cd ./TestStatusBarApp
rake

At this point you'll see an entry called "TestStatusBarApp" appear in your status bar, and clicking it will show you three options - a working about window, a custom action to show off how you handle menu item events, and a quit command. Note that the app doesn't appear in the dock or the cmd+tab window, as it resides solely in the status bar!

RubyMotion app in OS X status bar

How it works

As this is a RubyMotion template, it basically just creates the app skeleton for you, so we can delve into the code to understand better how it's working, and how we can build on it.

To make the app status bar only, and have the dock and cmd+tab window ignore it as a targetable window, we have added the following to our Rakefile:

app.info_plist['LSUIElement'] = true

We've also updated the spec/main_spec.rb to reference and test the status bar menu items instead of windows by default - you'll want to update this as you make changes to your menus.

The rest of the good stuff happens inside app/app_delegate.rb as you might expect - and it's surprisingly little code. In our applicationDidFinishLaunching method, we're setting up a new NSMenu, and then creating a status item to sit in the system status bar:

@status_menu = NSMenu.new

@status_item = NSStatusBar.systemStatusBar.statusItemWithLength(NSVariableStatusItemLength).init
@status_item.setMenu(@status_menu)

We're using NSVariableStatusItemLength so that the status item can match the content. It's easier to setup this way, but bear in mind that you probably don't want the content that'll appear in the status bar to be changing width too dramatically or too often, as that could be quite annoying!

Next up, we just set the highlight mode to true so that when you click the status bar item you get the blue highlight to show that you've clicked it, and we're also setting the title to the app name:

@status_item.setHighlightMode(true)
@status_item.setTitle(@app_name)

After that we can setup some of our menu items, but we're using a helper method to make things a little less verbose:

def createMenuItem(name, action)
  NSMenuItem.alloc.initWithTitle(name, action: action, keyEquivalent: '')
end

This just sets up a new menu item with the specified title and action (more on actions in a minute), and returns it. We can then use it like this:

@status_menu.addItem createMenuItem("About #{@app_name}", 'orderFrontStandardAboutPanel:')
@status_menu.addItem createMenuItem("Custom Action", 'pressAction')
@status_menu.addItem createMenuItem("Quit", 'terminate:')

Actions are just references to methods that will act as event handlers for the menu item being clicked. The first and third items have actions that are "built-in" to OS X apps - namely with the default app structure, the about panel is already there and ready to use, and "orderFrontStandardAboutPanel:" shows that window. It contains a list of credits derived from a text file in the "resources" folder of your project ("Credits.rtf"). Within our new template, this is still the same as the one that comes with the default OS X template for RubyMotion, and should be edited to reflect your own details. The third item, with action "terminate:", as you might expect refers to a method that is already accessible and that shuts the app down. It's important to provide a way for the user to shutdown your status bar app, as it doesn't appear in the dock or the "Force Quit" list!

The second menu item there is more interesting - "pressAction" is our own defined method, and acts as our event handler for a click on that item:

def pressAction
  alert = NSAlert.alloc.init
  alert.setMessageText "Action triggered from status bar menu"
  alert.addButtonWithTitle "OK"
  alert.runModal
end

This too is fairly basic - we're just popping up an NSAlert dialog to show that the menu item has been clicked, and the event handler has correctly received the event. That's all there is to creating a basic status bar app in RubyMotion!

Extending our app

Now let's extend our default app skeleton provided by the gem to do something a little more interesting. We'll have a menu item action that updates the content of the status bar item itself. First of all, let's update our spec to expect a fourth menu item:

it "has four menu items" do
  @app.delegate.status_menu.itemArray.length.should == 4
end

Then, in our app delegate, lets setup a new menu item above the "quit" item, called increment:

@status_menu.addItem createMenuItem("Increment", 'pressIncrement')

Lastly, let's implement our event handler:

def pressIncrement
  @count ||= 0
  @count += 1

  @status_item.setTitle("#{@count} times")
end

Here we're establishing a variable to keep track of our count, if one isn't already set, then we're incrementing it. Finally we're referencing our main status item (which remember refers to the item that sits in the status bar itself, not any of the child menu items that show when you click on the status bar item), and we're updating the title with the count. Now if you fire up the app, and click "Increment" from the status bar app menu, you'll see the value in the status bar update!

RubyMotion app in OS X status bar extended with increment action

From here you can see how it'll now be fairly straightforward to start building slightly more useful functionality into the app, either hooking into system stats or calls to provide data, or perhaps calling out to a third party API to do interesting things, making it all available from the status bar. In a future article we'll look at how to run things in the background to do something interesting that updates the status bar without direct user interaction.

Feedback welcome

In the meantime, check out the gem, and let me know what you think! Any questions or comments on the above are welcome, either below or @ejdraper on Twitter!

technicalrubymotioncodekickcode