Poolside FM

Poolside FM

I fucking love Poolside FM. It's like the perfect playlist for summer. The only problem is I always had to keep it open in a tab on my browser. I was constantly accidentally closing the thing and because it's a website, it's not controllable through the media keys on your keyboard.

I had a sift through their source and stumbled across their API. I tweeted that I'd love to build a little menubar app for Poolside. It would be a nice to have thing and I wanted to build my first Mac app.

The Poolside guys Marty & Grant seemed up for it too.

Building It

The app is actually pretty straight forward. It just uses and NSPopover to display a view from a storyboard. There's a model for the individual tracks and a collection class to fetch them from the Poolside API. Once I had that it was just a case of hooking it up to SoundCloud and shoving the stream link into an AVPlayer instance.

The first iteration was nothing pretty but it worked-ish…

Not pretty

I forgot to add an observer to detect when the track had finished playing so it stopped and you had to manually skip. Oh, and skipping was a pain too as the media keys weren't hooked up.

Adding an observer is just a case of getting the default NSNotificationCenter and telling it which selector to load when the AVPlayerItemDidPlayToEndTimeNotification is sent.

notificationCenter.addObserver(self, selector: "songFinished:", name: AVPlayerItemDidPlayToEndTimeNotification, object: song)

Adding support for media keys proved to be a littler trickier as there are a lot of apps that want to take control of them. Thanks to the guys at Spotify this is actually pretty simple as they open-sourced their SPMediaKeyTap library. Hook this up to a delegate and it's a simple as using a switch to detect which key was pressed to call the relevant method.

You have no idea how much I wanted to use 🍹 or 🌴 as a constant name while building this

Design

Marty designed up a slick look for the player and sent it over.

The hardest part of implementing this design was turning NSPopover white. By default it uses the translucent look that was introduced with OS X Yosemite. It was pretty simple to set the main view white but that left the arrow at the top looking out of place similar to the Adobe Creative Cloud app. Obviously, not ideal.

To fill the whole thing in we need to add a subview to our main view, set the frame to be identical to our window, and stack it below. We can do this by creating a new class with NSView as its super and then overriding the viewDidMoveToWindow method.

override func viewDidMoveToWindow() {
    let superView = window!.contentView.superview!
    let bgView = PoolsideBackgroundView(frame: superView!.bounds)
    
    superView!.addSubview(bgView, positioned: .Below, relativeTo: superView)

    super.viewDidMoveToWindow()
}

We fetch the window's superview which in this case is our NSPopover and load up our PoolsideBackgroundView. This is just an NSView that overrides the drawRect method to fill it with NSColor.whiteColor(). The NSPopover's appearance hasn't changed, we're just adding a new white view that's the same size and dimensions above it; in between the popover and our main view.

We also added an autoresizingMask for height and width to our bgView just in case. We're using fixed element sizes but you never know what might happen.

The final thing to do was to set the class of our view to PoolsidePopover in our storyboard.

Once that was done, everything was looking just like Marty's design.

SoundCloud Issues

While we were testing the app, we noticed that it would suddenly stop on a track and you'd have to skip to the next one manually. We'd added in error detection for missing URLs but I'd not anticipated the API not returning the correct status code.

Usually, when a file isn't found you'd expect to receive a 404. For some reason SoundCloud returns a 200 and just chucks the error in the JSON response:

{"errors": ["404 - File Not Found"]}

Sometimes, even if a track is streamable, Soundcloud just display a blank page with a 404 status header. This caused havoc with AVPlayer. I added an observer to check the current track status. If it got marked as AVPlayerItemStatus.Failed then the player actually had to be replaced with a new instance or it would fail to play subsequent tracks.

override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
    
        if player.currentItem?.status == .Failed {
            player = AVPlayer()
            nextTrack(self)
        }
    
}

Once I'd checked for that, we were plain sailing ⛡️

πŸ’»πŸ”Š = 🌴🍹

GO DOWNLOAD!!

Come Say Hello! Drop me an email, follow me on Twitter, or check out Cocoon (you totally should, we're doing some cool stuff over there).