There are few things more liberating than saying, “Fine, I’ll do it myself”. And then doing it. And that’s where this whole thing starts…
I’m a simple guy. I don’t like “stuff”. Yet, there’s an abundance of “stuff” in today’s mobile apps. I’ve come to terms with it as much as possible, but there’s been one thing I’ve been unable to get over. Playing music.
I want my music player to play music—and nothing else. I know I’m a minority in this view, but I’m not a fan of audio streaming services. I like to own my music and listen to it. I’m not into discovery or serendipity. I know what I like and that’s what I listen to. But today’s major music apps are more discovery engines that happen to play music rather than music players first. Discovery is hard and requires a completely different experience than media consumption. All of that adds complexity—which I dislike.
I’ve looked far and wide for an existing app that would meet my needs and was unable to find anything. If one does exist, well, it’s too late because I’ve decided to fix this for myself. Hell, if I can make a typeface, I can make a mobile app, right? Right?
This blog post will chronicle my process of making my first mobile app, similar to how I documented the creation of Olivia Sans. I’m fairly far along in development, so forgive me as you’ll be joining midway through the process.
This is the north star. This app is a music player and, thus, it will focus on playing music. There will be no favoriting or sharing or any other feature that isn’t directly tied to the process of playing music. And yes, this can be subjective. For instance, one could say that searching for music is critical to play music as you need to find it to play it. I’m interested to see how I develop my point of view in regard to what is and what is not directly related to playback.
I don’t plan to add any functionality for adjusting your library and/or downloading songs. Yes, this means I’ll be relying on iOS’s Music app to manage my library. Is this a pain? Maybe, but it keeps the app experience simple and given how infrequently I play with my library, I’m betting it’s worth it.
I am not a fan of how music apps have different control layouts for playback depending on where you are in the app. Typically there’s a Now Playing view which has large controls and then a minimized view with a subset. I want one playback interface that doesn’t change so I can rely on muscle memory for interaction and not be concerned with what view I’m on to do so.
If there’s one thing I accomplish with this project, I want this to be it.
I want it to be abundantly clear how this app allows you to interact with content. The goal is for this app to have two views: Library and Album. The Library view let’s you choose your desired album and Album view let’s you play your desired song. I’m not a fan of the “Now Playing” pattern as it diffuses the Library > Album > Song relationship. I don’t think I’ll need more than two views to make this experience work—and frankly, I think it’ll be better because of it.
There will never be an overflow menu in this app. If I can’t find room for a button/action/whatever, it doesn’t go in. This will be a lovely forcing function to keep this app simple. I also want to rely on the OS to handle as much as possible. If a user can easily do something outside of this app, I’d highly prefer to just leave it out.
There won’t be any settings either. I can see the minimal value in some settings like toggling cross-fading, but it just doesn’t seem worth it. The closest I would get to settings is the inclusion of a Library view switcher (e.g., Recently played, Albums, Artist, etc.), but I’m mentally fighting that tooth and nail.
Animated art covers are novel, but ultimately fluff. Ideally one is listening to music without looking at their phone. I’m not aiming for a radical departure in interface design. Ironically, I believe most music apps have developed sound interface patters—their primary flaw is that there’s just too much of it. I aim for polish and attention to detail, but I don’t see this interface breaking new ground.
So, this marks day one in my documenting the build of this app. I will be updating this post as I have updates to share. My goal is for this app to solve a very specific problem and nothing more. I also want this project to signify that anyone can do this with enough time and effort. I firmly believe that we’d all have far better options to choose from.
I need to start writing before this project is too far along. I’ve been at this for about a week and a half at this point and I’m much farther along than expected. I chose to write this app in SwiftUI and while it’s sped up initial process, it’s been painful at every step. I forgot how much I don’t enjoy declarative languages—it’s just not how I’m used to writing code. I can see how SwiftUI can be great once you’re up to speed, but the learning curve is steep. For every convenience I’ve be able to utilize, three head-scratchers have halted progress.
That said, I have a semi-working app. The interface is nothing groundbreaking—it consists of two views, Library and Album.
The Library follows the standard grid layout with persistent playback controls fixed at the bottom. I was hoping to remove album/band name labels, but that only works if you can rely on the album cover being available. I may explore a treatment where album/band is only shown if the cover art isn’t available. That would make the Library view as reduced as I can imagine.
Tapping into any album shows the Album view.
Again, nothing mindblowing. If anything, it’s the lack of elements that stand out. There’s the barest of metadata, no album-level actions and no track level actions. After using this view for a bit, it’s been refreshing just having less to look at. The playback controls persist on the Album view as well, so playback can be adjusted the same way while navigating through albums.
I’m not sure what I think about the cover art being as large as I currently have it, given how much it pushes the album tracks down. I might toy with alternative layouts to make the Album view a smidge more utilitarian.
Much of the work in the past view days have been refining the interaction within the playback controls. That little piece of UI is the heart of the app, and so it needs to put in a lot of work. I’ll go into that area of the app in more detail in a future entry.
Next up is to migrate my views to utilize NavigationStack so that I can more easily implement a nice shortcut to the active album from the playback controls interface. From there, I’m on to code cleanups and bugfixing to hit my first milestone.
Progress is speeding up. I continue to not enjoy building in SwiftUI, but I’m understanding it more. I went through a pretty exhaustive cleanup after accumulating a lot of debt in the initial build. It’s still a mess, but not a catastrophic mess. I think I’m about 3-4 days away from being ready for an alpha-ish build—which means I’m still a long way away from completion.
I took the leap and removed all album/artist labels in the Library view. That means I’ll need to create a variant of the album preview component to display album/artist name when the cover art is unavailable. It’s worth it though as it dramatically simplifies the Library. And yes, I’ve temporarily blurred the album covers because I’m not interested in sharing my entire music library.
I still need to take on the NavigationStack migration, but the cleanup/refactor should make that considerably easier. After that, I have one more major issue to fix and then the app will be heading to TestFlight. I’m trying to defer all visual/interactive refinements until I’m in the TestFlight phase as it’s far too easy to rathole in pixel peeping instead of ensuring the app actually works. I have a plethora of refinements planned, but I’m forcing myself to take my own advice and focus on the basics for now.
All main features are now built. I’ve also left a trail of codebase destruction in my wake. I’m nonetheless proud that I have a release build ready after a couple weeks of work. Here’s some of the functionality I’ve added since the last post:
The currently playing track is now tappable and takes you directly to the album. I’d like to improve this feature by scrolling directly to the currently playing song.
I added the ability to sort the library by date added and most listens. I didn’t enjoy adding this feature, but at least it’s hidden above the library on first load.
I don’t think it’s reasonable to have to terminate the app to have newly purchased tracks show up in the library. This feature allows you to refresh your available songs/albums, and luckily there’s no button added on the screen to do it.
This is the one addition that I think I’ll be using the most. 90% of the time I’m just opening the app to play an album from the beginning. This quick access interaction is something I’m already using every day.
I want to work on refinements, but I know the right thing to do is clean up the mess I’ve made. I’ve also begun to share the app out with friends through TestFlight. I was avoiding like the plague, but it’s clear I’ll have to add search functionality. Just another task to put on the list.
My last update was 10 days ago, but that feels like an eternity…
There’s been so much progress on the app since the last update. And even more frustration. I’m glad I took this project on, but it’s been painful and not in ways that I think I’ll look back on appreciatively. Developing for iOS is just painful in ways that doesn’t feel necessary. I’ll spare you the gory details, but I ran into a couple bugs that were beyond frustrating.
That said, through all the frustration, I have a build ready for public beta. It’s still missing some key improvements I’d like to get in, but it’s ready enough to share with strangers. Here are the latest updates:
So, while imperfect, it’s time to let this bird out of the nest. The TestFlight build will be made public tomorrow. Speaking of which, the TestFlight experience was pretty awesome. I had consistent support from some good friends who gave me continuous feedback. I also got a huge assist from a friend to fix a bug I wasn’t able to address. They’re why I’ve decided to make this public.
As much as I know the codebase could improve, the stability of the app has been shockingly good. The crash rate is at 0.12% with the last one occurring 2 releases ago. If this public beta gets used with even the slightest amount of volume, I expect that number to go up significantly.
And this public beta is going to be a major influence on what comes next. I’m admittedly tired and not ready to pour an unlimited number of hours into this app with no realistic goal in sight. In many ways, I’ve far exceeded my initial goal of building an app that met my needs. The rest is a bonus.
So, if the public beta goes well and there isn’t some existential flaw with the app, I will be putting it up for sale on the App Store. Which is bonkers and not something that I expected would come of this. Why? Well, damn, why not? I’ve learned so much from this process already, why not just jump into another learning experience related to App Store submission and marketing. But, I shouldn’t get ahead of myself. Let’s see where this public beta goes…
Well, it’s been a ride. The public beta went… not great. Abysmal would be another way to describe it. My goal is to have 0.1% crash rate to be confident actually putting it on the App Store. The crash rate for the public beta was roughly 25%. I’m no math wizard, but by my calculations, 25% is significantly higher than 0.1%.
That was the bad news. And, yes, it was pretty bad. I spent a full day trying to decipher crash logs and sending numerous test builds to an overwhelmingly generous individual. No success. Not even progress. I seriously contemplated just hanging up the project given the lack of progress and my complete unwillingness to ship something so busted. I took a few days off and just let it go.
The good(ish) news was that every crash had the same crash report. They all failed in the same place and in the same way. Meaning that if I fixed this one issue, it should fix the only issue folks were running into. I just had to figure out how the hell to do it.
So, after my little break, I got back to work. After some dense reading, I was able to discern that the crash was memory related and it was specifically failing while instantiating album cover art. My personal music library isn’t small, but it isn’t gargantuan either. So, on a whim, I decided to not store all the album art on initialization, but rather query/instantiate album covers only when they displayed on the screen. Meaning on the library I would load the image data when the album preview scrolled into view and nil it out when it scrolled out of view. I noticed a significant drop in overall memory usage, especially on initial load. I crossed my fingers, sent a new build to the aforementioned generous individual and waited.
Luckily, the launch crash was no more. Man, that felt so good. I shipped out a new build for TestFlight and while I have no doubt I’m jinxing myself, there have been no reported crashes to date. This is such a major milestone and I’m back to being cautiously optimistic. I’ve spent the last couple days cleaning up some tech debt from bug triage and prepping for the last few pre-launch refinements.
It’s been a hot minute since I’ve posted an update. A ton has happened between now and the previous post—most notably submitting to the App Store! Here are some highlights of what I’ve done/learned recently:
Without a doubt, addressing memory usage issues has been the most time-consuming and frustrating part of the entire process. Album art can take up a lot of memory and since SwiftUI handles garbage collection on its own, I don’t have the ability to manually release memory. So, much of my time has been working towards reducing the baseline memory footprint. We’re finally at a good place where the app should be able to handle music libraries with many, many albums. I do wish there was a way in SwiftUI to manually clear out memory. Maybe there is and I’m not aware, but I’m just glad we’re in a better place.
While dealing with memory issues was painful, localizing the app was a joy. I was really impressed with how XCode provides a localization table for different languages and an easy API for abstracting strings. I wish there was an equivalent for the web platform, because man was it so easy to implement. I was super, super fortunate to have many generous folks contribute translations for the app. Thanks so all those people, the app is shipping with translations for Simplified Chinese, French, French-Canadian, German, Japanese, Brazilian Portuguese, Russian, Spanish, Latin American Spanish and Ukrainian.
I’ll be honest with you, vanilla gradients in Swift are not great. I typically use dark mode on my phone, so I didn’t notice just how bad they were until debugging the app in light mode. I was ready to just ditch light mode altogether—that’s how bad I considered it looked. I made a last-ditch effort to improve the gradients by implementing eased gradients and it made all the difference. It’s the only reason I’m shipping with a light mode version of the app.
Yes, it sure would be nice if I had before/after examples of this. Unfortunately I don’t. Suffice to say, the new gradients have a much smoother spread and the default gradient Swift renders.
If memory usage was the most frustrating part of the development process, implementing scroll logic was definitely second. I cut my teeth on learning scroll logic with the top library controls reveal. That experience provided enough background to help me create scale/blur effect on album scroll.
There’s so little to this app that I wanted to have some level of dynamism—especially in the album view. I’m frankly not super happy with many of the current motion treatments, I’m hopeful I can get some help from some peeps in the near future.
While it’s far from perfect, I have a working build that’s ready for public usage. If you asked me about a month ago, I would have said it wasn’t going to get to this point. I’m equal parts shocked and elated. Hopefully I’m able to get through the App Store approval process relatively unscathed…