Last Updated: August 09, 2016
·
3.687K
· jonasnielsen

Dockless RubyMotion menubar app with start at login

This is a guide to create a RubyMotion OS X app that runs on the menubar (without a dock icon), is compliant with App Sandboxing and capable to launch at login. Although this sounds fairly straightforward I found it a bit more complex than anticipated. The following process describes a solution that should be accepted to the App Store (I haven't submitted it yet). It isn't perfect, but it will take you far.

Here's what you'll need to setup:

  1. A RubyMotion OS X menubar app that doesn't display a dock icon
  2. Another RubyMotion "helper app" with a dock icon. This app is stored within the main app. It's launched by the operating system and does nothing but start the menu bar app and close itself again.
  3. Modify the build process of your primary app to include the helper app
  4. A small addition to your primary app to tell the operating system to start the helper app at login (which subsequently launches the primary app)

To get a better understanding why all this is necessary, I recommend you read Delite Studio's Blog Post. If you're just a busy cowboy, keep reading.

Before you start, make sure to have a clean git working copy. None of this is intuitive, so make sure you can discard the changes easily.

1. Configuring your menubar app

So you've decided that your app should only run in the menubar. Welcome to the cool kids! Let's hope there's space left for you in the VIP-area at the top of the screen.

I'm guessing you already created your app. Perhaps you even configured it to run as a dockless menu bar app. If not, add try adding this to your Rakefile:

app.info_plist['NSUIElement'] = 1

As for the menu bar icon, you'll want to do something like:

def setup_menu_bar
    @status_item = NSStatusBar.systemStatusBar.statusItemWithLength(24.0)
    @status_item.setHighlightMode(false)
    @status_item.setImage(NSImage.imageNamed("Status"))
    @status_item.setTarget(self)
    @status_item.setAction('clicked_menu_bar:')
end

def clicked_menu_bar
  p "You'll take it from here"
end

Not a lot of surprises. Here's how to make it launch at login.

2 Adding the helper application

It's Apple's recommendations that you use a helper application to launch your menubar app. In RubyMotion, this means creating a new OS X project and include it in the build process of your main app.

Thankfully the helper application is very simple. I've created a RubyMotion template to make the setup process a bit easier. In the following MyRubyMotionAppName should be the current name of your menu bar app.

cd into your menu bar app RubyMotion project:

$ cd ~/path/to/MyRubyMotionAppName

Inside that project, create a new project using the custom app-launcher-template:

$ motion create --template=git@github.com:JonasNielsen/app-launcher-template.git MyRubyMotionAppName-app-launcher

Make sure to rename MyRubyMotionAppName with the name of your menu bar app.

Basically, all this app does is launching the menu bar app and close itself again (/MyRubyMotionAppName/MyRubyMotionAppName-app-launcher/app/app_delegate.rb):

class AppDelegate
  def applicationDidFinishLaunching(notification)
    app_name = "MyRubyMotionAppName"
    if NSWorkspace.sharedWorkspace.launchApplication(app_name)
      NSApp.performSelector("terminate:", withObject:nil, afterDelay: 0.0)
    else
      raise "Could not open app with name #{app_name} - is the name correct?"
    end
  end
end

cd into your helper application and build and run it to confirm that the menu bar app is launched:

$ cd MyRubyMotionAppName-app-launcher/
$ rake 

You probably want to update the Rakefile of the helper application with a correct app.identifier and perhaps a dock icon.

3. Include the helper application in the build process of your menubar app

Add this to the bottom of your Rakefile.rb:

class Motion::Project::App 
  class << self 
    # 
    # The original `build' method can be found here: 
    # https://github.com/HipByte/RubyMotion/blob/master/lib/motion/project/app.rb#L75-L77 
    # 
    alias_method :build_before_copy_helper, :build 
    def build platform, options = {} 
      # First let the normal `build' method perform its work. 
      build_before_copy_helper(platform, options) 
      # Now the app is built, but not codesigned yet.
      destination = File.join(config.app_bundle(platform), 'Library/LoginItems') 
      info 'Create', destination 
      FileUtils.mkdir_p destination 
      helper_path = File.dirname(__FILE__)+'/MyRubyMotionAppName-app-launcher/build/MacOSX-10.8-Development/MyRubyMotionAppName-app-launcher.app'
      info 'Copy', helper_path 
      FileUtils.cp_r helper_path, destination 
    end 
  end 
end 

Double check the helper_path and rename MyRubyMotionAppName with the name of your app.

Now when building your menu bar app, your helper application should be contained in the Contents folder of MyRubyMotionAppName.app.

All that's left now is to allow your users to launch the app at login. This is done with the ServiceManagement framework. Add it the Rakefile of your menubar app:

app.frameworks += [ 'ServiceManagement']

Finally, integrate following function with your UI:

def start_at_login enabled
    url = NSBundle.mainBundle.bundleURL.URLByAppendingPathComponent("Contents/Library/LoginItems/MyRubyMotionAppName-app-launcher.app") # path
    LSRegisterURL(url, true)
    unless SMLoginItemSetEnabled("com.your-name.MyRubyMotionAppName-app-launcher", enabled) # identifier
      NSLog "SMLoginItemSetEnabled failed!"
    end
  end

Again, make sure to double check identifier and path to your helper app.

Final caveats

That's it! You should be good to go now. Don't forget that Apple forbids auto-launch at startup without explicit consent from the user. Unfortunately, you can't just throw the above piece of code into your AppDelegate.

Also, rumors are that launch at login using this method will only work if the app is placed inside the user's /Applications folder (because of App Sandbox requirements). Since you plan to distribute via the App Store, that shouldn't be an issue.

Credits

I would never have figured out how to do this, if it wasn't for the help of a few fine gentlemen. All credits goes to Delite Studio's Start dockless apps at login with App Sandbox enabled and Eloy Durán's invaluable RubyMotion interpretation of the article

Thanks guys!

Eloy @alloy
Me @jonasnielsen

2 Responses
Add your response

This helped me immensely! Thanks for posting it!

over 1 year ago ·

This is awesome. Thanks for sharing.

over 1 year ago ·