Building Doppi: Engineering Through Design
My journey building a simple music player
A little over a year ago, I wrote about the beginnings and design philosophy of my music app, Doppi, a simple music player. I also promised a second part, about engineering. It took a while, but here it is!
Fair warning: this post gets pretty technical.
I am, first and foremost, a designer. I started out in my pre-teen years designing websites in Photoshop and Dreamweaver, but then quickly moved on to HTML/CSS, with a bit of jQuery. I got a bachelor’s in Design, while also learning to code native iOS UIs in my own time since 2011. I had no real experience programming, so I did what any sane person would logically decide to do: dive head-first into Objective-C and Cocoa Touch.
It took me two and a half years to finally know what I was doing in Xcode. I had been working with a friend on an old app that was the very definition of spaghetti code — it was so bad, we couldn’t even update it for the 4" iPhone 5, because many things would break. So after iOS 7 rolled around, I used the model objects and API bridge that my friend had built in a previous reboot attempt to re-write from scratch all views and controllers. It was a great exercise in UI development for me, and it felt great. I could finally own the bits and pieces that were so alien to me before.
Another year and a half passed, and I had fully redesigned that app, twice. It was fun, but I wanted something new to work on. And so, Doppi got started. I really wanted to try new things, so I set the language drop-down to Swift on that new .xcodeproj, and got to work.
A Remote Control
Doppi seemed like the perfect project for practicing UI development. The Media Player framework had all I could ever need to develop a music library app, and MPMusicPlayerController had simple enough methods to control playback. I didn’t even have to worry about writing my own player, because Doppi could just tell the system Music app to play what the user selected, essentially acting like a remote control for it. I liked that, because it made things so simple (for me). Sure, I couldn’t have any control over the playback queue, but for a long time, the Music app didn’t have that either, so who cares, right? Right. So I got to work.
One of the things that bothered me when I started writing code for iOS was the lack of a way to define styles globally. I was coming from the web, where CSS — like it or hate it — gets the job done. So it was exciting to discover that I could have a Swift struct filled with static constants such as ‘albumCellArtSize’ or ‘primaryLabelTextColor’ to store and use consistent values and quickly alter the visuals, globally. (I now realize this can also be accomplished in Objective-C, but it just wasn’t very inviting to do so.) Trying out new ideas and adjusting a shade of grey just-so became really easy. I was a happy camper. (Well, for the most part — Swift 1.2 was a land full of AnyObjects.)
So what’s next? Well, one piece of feedback I kept getting from my friends was that, whenever they tapped the currently playing song on Control Center, they would get thrown into the Music app instead of Doppi. Huh, that’s not great. But also, they would love to have control over the playback queue. That sounds awesome from a user’s perspective, but for me, still a relatively novice programmer, it sounded daunting. (Interesting fact: I was working at Google Search as a UX Engineer at the time. But I was just building native UX prototypes, which is actually a lot easier than writing a full-blown music player.)
After willfully avoiding it, I apparently had to write my own player now. So, with help from the cool people at Gangverk, who had generously open-sourced their GVMusicPlayerController class, I got to work. I couldn’t just drop their class in and call it a day, because I wanted my app to be pure Swift, and I don’t really like using external libraries that I don’t control. (To this day, there are only three very specific classes in Doppi that I didn’t write, and I just copied the 3 files over, because I don’t like package managers bloating my project and build process.) So after months of sitting down in my spare time with one eye on Gangverk’s code and another on mine, I wrote my own AVQueuePlayer-backed music player, complete with a fully editable playback queue. Such fun!
It was a mess, though. The Player.swift file was over 850 lines long. It quickly became clear that I had to break the queue apart as a separate component, and standardize its methods:
Also, before releasing, it became evident that Doppi needed to preserve its queue when the app was re-launched, so I implemented a Plist-backed mechanism to store and restore the queued songs. What could go wrong? Well, on an iPhone 6s, it could take over half a minute to restore a 1,600-song queue, because I could only store MPMediaEntityPersistentIDs on my Plist file, and then restore the actual MPMediaItems by running MPMediaQueries for each ID. And I was doing it twice over if your queue was shuffled, because I wanted to preserve the original (non-shuffled) queue, plus the exact same shuffled queue you had before leaving the app for consistency. MPMediaQueries are relatively slow, which is fine for most use-cases, but it really shows when performing them thousands of times in a row. But I couldn’t figure out a smarter way to do it, at the time.
So I was a bit frustrated with what I had, but I shipped anyway. It worked. To give the user enough functionality while the queue was still restoring, I restored the 7 upcoming songs first, so you’d have something to skip to if you wanted to do that. It was… fine, I guess. Not great by any measure, but it was the best I could do at the time (and it’s the way the app has run for well over a year now).
But then I kept thinking of even more great features, and many of them required editing a song’s metadata. Cold-launch time wasn’t great either, and it was becoming apparent that it was because the MediaLibrary framework queries just aren’t that fast. Soon, it was clear that I not only had to blow up my Player class and build a new, separate Queue class, but I also would have to re-engineer the library model objects. I would have to move away from the Media Player framework and build my own, custom object graph, and write a library importer/updater to make it all just work. It was a major brain transplant for Doppi. More than anything, it was intimidating: I had never done anything like it. I knew it would take a while, so I put it in the back burner. I was eager go back to improving the UI.
2.1 and 2.2
Back to UI
I’ve always thought Doppi should be a very personal app. Your music collection can be a very intimate part of who you are, and it can define your state of mind at any given moment. So whenever you see Doppi, you should be able to see yourself reflected in it, both through album art and through the UI. So in 2.2, I built a system to apply a color theme to the entire app based on the struct/static variable approach that I began building in 1.0, a simple protocol that defines one method (‘themeDidChange’), NSNotifications, and two methods on AppDelegate that traverse the entire UIViewController and UIView hierarchy sending that one message to every class that conforms to the ‘Themeable’ protocol when the theme changes.
The user can choose between 5 options of highlight (tint) color and between light and dark mode. This, plus some other playback and navigation options, surfaced the need for a settings screen. I wanted this screen to pretty much build itself, because otherwise, maintaining it would eventually become a big pain you-know-where. So I built classes for every type of supported preference, simplified ways to read and write user defaults, a standard way to represent the preference list and its groups, and a UITableView that could interpret it all, and voilà: Doppi now has Preferences.
Here’s a big spoiler: I eventually want to make an iPad version. The Now Playing view needs to reconfigure itself properly for the big screen. Also, this was the first view that I built for Doppi, and I had learned a lot in the meantime. The view was originally built completely without the use of Interface Builder or Auto Layout, and each and every button, label, and slider on it was created, configured, and laid out manually and programmatically in a single 1000-line UIView subclass (!!!). So I blew it up, redesigned it in Sketch, and rebuilt it from the ground up using components. Each component groups together functional sections of the view into manageable blocks that can be easily repositioned. They have very lightweight implementations and their layout is described in a .xib file. Everything is composed together in a parent view with a .xib that manages the overall structure (and can potentially adapt for different size classes), while the action methods are piped up through composed delegation from their particular classes to the Now Playing view controller.
These were very refreshing versions, but after pushing them out, I knew I couldn’t avoid the brain transplant any longer. It was the only way to write new meaningful, well-implemented core features — Doppi was stuck without it.
A brain transplant
So: I needed new model objects for artists, albums, and songs, plus playlists. They had to be imported from the user’s on-device library, updated when that changed, persist between launches, and store custom metadata. At first, I had no idea what I was doing, so I obviously decided to use Core Data. Obviously. I had zero experience with any kind of database, but the docs praised the transparent and efficient abstraction layers that would lead me to only write Cocoa objects and not really worry about anything else. Until I decided to change the object structure in any way, then I had to worry about migrating. But that was it, right? Well, not so much. I worked on a library implementation based on Core Data on and off for months, and it felt like I was getting nowhere. There had to be a simpler way.
Enter NSKeyedArchiver. A simple and awesome way to store an entire object graph to a file on disk and then restore it. It’s easy to implement, easy to use, and pretty fast. Practically magic. For the longest time, I had no idea that it existed, but discovering it was amazing. It just goes to show how deep iOS really is thanks to its macOS and even NeXTSTEP roots.
From the Core Data exercise, I grabbed the general object structure and generated what I call blueprints, which are protocols that define the minimum acceptable implementation for each of the core library objects, and dubbed them ‘Specs’. There’s SongSpec, AlbumSpec, ArtistSpec, and PlaylistSpec. Using those, I wrote different implementations of the actual objects, which I could very easily replace using typealiases that, for instance, linked the Song type to whatever SongSpec-compliant class I was testing at the time.
This exercise has a very neat side-effect: since most of the objects that use and interact with Song, Album, and Artist are abstracted away from the actual implementations, most of Doppi’s views and controllers are potentially now ready to display music from any source (such as a streaming service or a non-iTunes local music file), as long as there are objects that interpret that source into what the Specs require. At one point, between being fed up with Core Data and getting started with NSKeyedArchiver, I was looking for a Plan B, so I wrote classes that just wrapped an MPMediaItem into a SongSpec-compliant class, plus equivalents for Album and Artist. Not bad.
Importing the library and building the object graph was not a trivial problem, particularly because, since I don’t have any formal CS training, I’m very insecure about my architectural decisions. I get double-worried about whether something actually makes sense, or if I’m duplicating/leaking objects, and so on. I finally decided to take an artist-first approach to the graph: an artist owns albums, and albums own songs. Objects get imported and created in that order to make sure their relationships are sound. (Get it? Music app? Sound? You get it.) Of course, there are ‘orphan’ albums and songs, and those are accounted for in the import process as well.
Progressively updating the library was a problem I avoided for a long time. Many alpha and beta versions of Doppi 2.3 actually performed a full library import at every cold launch, which obviously isn’t very efficient. I eventually settled on a model that checks for deleted tracks and removes them first, and then proceeds to re-create all artists that have changes. The library then swaps the outdated artists with the updated versions, adds in the new ones, re-creates the albums array by flatMapping the albums from the new artists, and then the songs array by flatMapping the songs from the new albums. Not very precise, but a lot more efficient than just re-creating everything (well, almost) again from scratch.
But what about preserving custom metadata? I haven’t gotten around to that yet, that’s why 2.3 doesn’t have any features that would require it.
One interesting problem was artwork storage. If I had my own object graph, I would probably need to store my own album art cache. That could allow me to perform all sorts of optimizations that would make the UI faster, like pre-rendering scaled versions with baked-in corner radii. The issue was, how do I make sure I’m creating and storing these optimized versions only once? I don’t want to waste precious battery life and storage space on duplicates. A friend suggested creating a hash string from the pixel data and using that to see if the image already exists. Great! Little did I know that creating a hash string was a processor-intensive operation and that there was a big potential for high memory pressure if I didn’t make sure the data got released in time. One beta tester with a particularly big library reported strange crashes on the initial library import process —as it turned out, the OS was killing the app for abusive memory footprint. So artwork was something that sounded easy at first, but actually ended up involving many hours of testing, debugging, and optimizing. Such is life.
As I mentioned, this new object graph is a major brain transplant. A lot of the work was adapting the existing views and controllers to use the new objects — a long task that actually resulted in less code. But also, a big chunk of the work was re-writing and re-structuring the player and queue from scratch. On the player side, I devised a high-level abstract class (AbstractPlayer) that could define a basic blueprint for player functionality, with bare-bones implementations that mostly just track state and handle things like remote control input and now-playing info reporting. This is designed so any concrete subclass can implement its own playback technology and logic, while preserving a clean and clear interface for playback control. On the other hand, with the new Queue class, I had a shot at fixing the main pain points of the old queue: interface clarity and restoration. The latter was fixed through NSKeyedArchiver, while the former, well, take a look:
That was, in very broad strokes, Doppi’s brain transplant. Even though it was a huge effort, a big learning experience for me, and though it will pay off bigly in the future, it should go largely unnoticed. The new Doppi should work at least as well as the old one, and that means it could look the same, too — but I couldn’t bear it. The thing that keeps me going is the desire to build a great product, and that means the design can’t rest on its laurels. It has to feel fresh, and it has to evolve in meaningful and useful ways.
Honestly, I just couldn’t help myself.
And UI improvements
For a while, Doppi has had a pull-to-search interaction in all the root library lists. You could pull down beyond the limits of the tables to reveal the Search UI. I wanted to build this interaction into every screen on the app, no matter the navigation level, but the way it was originally built and designed didn’t allow for that, and was starting to look dated. The new Search is available from anywhere in your library and it doesn’t just look great, it’s also handy: you can now trigger the top result by hitting the return key, so there’s no need to stretch your fingers reaching up.
Search actually inspired a big effort to finally standardize the typographic hierarchy across the app. This led me through a lot of testing and exploration, which was made easier by the use of internal type styles. Inspired in part by iOS 11, I settled on making font weights heavier and text colors a bit darker for increased contrast and better readability at small sizes. WWDC17 also inspired me to implement full support for Dynamic Type, so this is the most readable version of Doppi yet. Almost every icon in the app was improved in one way or another, and new icons were added to complement the headers of Search and the artist detail screens. This last screen got a big visual overhaul that includes refined typographic alignment and a custom implementation of large titles, which were also added to the playlist detail screen. Many other things, big or tiny, got some degree of attention. I believe I left no stone unturned (and then re-aligned).
It’s the most carefully considered version of Doppi yet.
Tomorrow Comes Today
This is not the story of a very advanced software project, and it’s far from being the journey of someone extremely smart solving impossible problems that will change the world. It’s a story about a simple music player that was built by someone who can no longer describe himself as just a designer. It’s the story of how I confronted every difficulty that crossed my path, of how I climbed every metaphorical mountain and navigated through every metaphorical forest to get my little app where I wanted it to be. And of how my desire to build something great through design guided me and pushed me to become a more confident software developer, which is a line of work I’ve admired ever since I was an 8-year-old boy staring at his brand-new lime green iMac G3.
Learning to write apps is hard. It requires perseverance above anything else. It would have been very easy for me to give up at any turn, at any apparently insurmountable frustration. (And there’s been many of them, with many more to come.) But I didn’t give up. I hope this story inspires you, no matter what you’re doing, to keep going, to never give up. Follow your vision. Chase it until you catch it, and then go further. Never settle.
There’s still a lot more that I want to do with Doppi. And now that it has a new brain, it will be a lot easier. You can get the brand-new, much-improved Doppi 2.3 from the App Store today.