Skip Tunes for Mac
Last week, my latest client project went live on the App Store. I’m thrilled to (somewhat belatedly) announce Skip Tunes, a menu bar app for controlling iTunes, Rdio, and Spotify. It’s on sale right now for just $0.99 on the Mac App Store.
Greg Dougherty, my client, has done a great job getting press attention for the app. So far, it’s been mentioned on The Unofficial Apple Weblog, CNET, Lifehacker, Cult of Mac, Macworld (3.5 out of 5 Mice) and a few other websites. Not long after launching, it even climbed to the #1 Music app in nine different countries!
Behind the Scenes
Warning: serious technical details ahead.
Chameleon
When I started working on the custom UI for the app, I tried to use the AppKit collection of classes:
NSView
, NSButton
, NSTextField
, and friends. It didn’t take long before I was really frustrated with
how hard it was to customize the appearance of these controls. For example, on iOS it’s easy to use
a custom image as the background of a UIButton
, you just call setBackgroundImage:forState:
.
Using that method, you can even specify two different images: one to use normally, and another
to use when the control is actually being pressed.
Getting the same thing done with NSButton
is not so easy.
The most straightfoward way to get a custom image is to subclass NSButtonCell
and
override it’s drawBezelWithFrame:inView:
method, which is not at all obvious if
you’re not familiar with the way NSControl
uses cells for drawing.
Then, for each button, you have to instruct it to use your custom
cell class instead of the default class.
To show a different image when the button is pressed, your implementation of that
cell method has to inspect the isHighlighted
property to see if the user
is holding down the mouse button.
When drawing that NSImage
, you need to make sure you use the right drawing method that respects
the flipped or non-flipped setting for the image and the graphics context.
None of the above is rocket science, but it’s a lot of work for what seems like a really easy task. (Kudos to the UIKit team for taking the opportunity to rethink and clean up these interfaces.)
Instead of wrangling all this myself, I took a shortcut and used an open source framework called Chameleon.
Created by The Iconfactory, Chameleon is a re-implementation of a big chunk of UIKit on top of AppKit. In a nutshell, it lets developers write iOS code that runs on OS X.
Chameleon really shines when you want to use the same code to produce both an iOS and a Mac app, but in this case it was worth it just to use the better APIs from UIKit.
I was able to write most of the Skip Tunes user interface
using UIView
, UIButton
, UILabel
, and even UIViewController
in addition to AppKit
classes like NSStatusItem
and NSWorkspace
.
If I had to do it over again, I think I would still use Chameleon, although I think I would try a bit harder to get AppKit to behave. I generally don’t like using cross-platform toolkits, but in this case the result was good enough to make up for it.
Scripting Bridge
The job of actually controlling and inspecting the media players falls onto AppleScript. Luckily, all three apps have similar scripting interfaces, so it wasn’t hard to create an abstraction layer on top of them.
Along the way, I learned a couple of neat tricks about Scriping Bridge, which is an API for using AppleScript from Objective-C.
To generate header files for each app, I used two command line tools: sdef
and sdp
.
Together, they take the scripting definition for an app and output an Objective-C .h
file that you can include in your app. Here’s how I generated the header for iTunes:
sdef /Applications/iTunes.app/ | sdp -fh --basename iTunes -o ~/Desktop/iTunes.h
The basename
is used to generate some of the object names in that header file.
In the above example, the iTunes.h
file contains classes named iTunesApplication
,
iTunesPlaylist
, etc.
I also learned that the SBApplication
object, which represents your app’s connection
to the scriptable app, can have a delegate that it reports errors to. This was
really helpful during development, because I could see where things were going wrong.
Distributed Notifications
If your app needs to respond to the behaviour of other apps, the way that Skip Tunes
needs to respond to the media player starting, stopping, or changing tracks, you
need to see if NSDistributedNotificationCenter
has the information you need.
Some apps will broadcast notifications over this channel about their state, which can
save your app from doing nasty polling.
For example, whenever the state of playback changes, iTunes publishes a
com.apple.iTunes.playerInfo
notification on this notification center. Instead of
checking iTunes’s state over Scripting Bridge every second, I just register for
this notification and wait to hear back.
If you go down this route, though, make sure to listen to NSWorkspace
notifications,
too, since opening and closing apps doesn’t usually send those state change notifications.
Go Get It!
You’re still here? Go buy Skip Tunes, already!