Under Cover

Multiple device mockups "running" the Under Cover app.

Under Cover is an album cover guessing game for music nerds. Something to kill time while waiting for your friend to join you in line at a show. Or, something to pick up every now and then just to remind yourself of your incredible, unparalleled music knowledge.

It’s currently available for iPhone and iPad on the App Store and the code is up on GitHub.

These are some notes on my experience making the app and some of the decisions that went into it.

Gameplay loop

The gameplay is very simple: Each round, the user watches an obscured album cover slowly reveal itself and tries to guess it as quickly as possible. The quicker the correct guess, the better the score. Users can customize the number of rounds (as well as some other parameters if they delve into the settings screen.) That's about it.

I served all roles in the making of the app myself, from concept to design to coding to writing the App Store copy. I wanted to make a small-scale, fun, and modern SwiftUI app with a strong foundation upon which to build. I also wanted to challenge myself on how quickly I could execute. All told, I spent just over 90 hours on it, from creating a new Xcode project to publishing version 1.0 on the App Store. (Though, of course, there is much more work to be done to improve and expand it over time.)

Dev Notes

The app makes use of the MusicKit framework, which handles everything from Apple Music audio playback to fetching album metadata. SwiftData and the Observation framework are used for the model and controllers, respectively. It is 100% SwiftUI with a layout that adapts for both iPhone and iPad.

Before each game the user selects a category, i.e. a pool of albums from which random selections are presented to the user. The album model stores minimal data about an album: album title, artist name, a URL for the album artwork, and the album's ID on Apple Music, as well as a simple boolean to track whether it's been favorited by the user.

I chose to go with an album artwork URL rather than storing the image itself in the model for two reasons. Firstly, storage. Even optimized, those kilobytes can add up quickly! Second, even though album artwork is provided freely with few-to-no qualifications through Apple's own MusicKit API, from past experience I knew that actually storing artwork in-app could risk displeasing app review due to their, um, passionate stance on the copyrighted material of large businesses such as record labels.

This tied in to another concern, which was that of where the above-mentioned categories would come from in the first place. Having the user add albums manually to construct their own categories would ruin some of the surprise. Adding my own preset categories was something I wanted to avoid, at least for the time being, out of an abundance of caution due to those concerns about copyrighted material. It would be best to have an outside data source for albums which could present them grouped in some way that the user could browse and add from at will.

Playlists! Apple Music features public-facing playlists created by themselves, users, and outside curators. While the user has to be a subscriber in order to listen to a playlist, they do not in order to just read its metadata. And once we can do that, we can make separate requests to the MusicKit catalog for all the tracks' individual albums with their covers and other metadata — everything we need for the model.

A screenshot of Under Cover showing a sheet with a textfield where the text “punk r” has been entered. Below is a list of punk playlists from Apple Music.
The playlist search live results interface.

To facilitate this I implemented playlist search with live results presented and updated as the user enters their query. This involved managing multiple asynchronous music catalog resource requests so as to provide a responsive UI with meaningful feedback for the user while avoiding slowing things down.

After watching Alex Nagy's video on MVC in SwiftUI I was eager to try out a similar pattern here. The app uses two controller classes, both made using the @Observable macro and initialized and inserted into the app environment at launch: a single player game controller to manage gameplay and an Apple Music controller to monitor and react to things like subscription status and user authorization.

After the user has selected a category, set the number of rounds, and initiated gameplay, a GameState enum is initialized and changes to its value determine the user interface.

In addition to letting the user set the number of rounds before each game, the settings screen features further game customization options, such as a black & white mode for increased challenge. All of these settings are stored in User Defaults, with @AppStorage leveraged for use within views. Lately, in most of my projects, I’ve taken to creating a simple StorageKeys enum to keep things tidy when going between @AppStorage and accessing UserDefaults directly (the former being a wrapper for the latter.)

A screenshot of the settings screen. It is a list which feature settings for gameplay configuration and whether or not to play music, as well as links to a game overview and photo credits.

As noted above, all of the MusicKit features covered up to this point are available whether or not the user actually pays for Apple Music. MusicKit's functionality as a catalog data source only relies on the developer registering the app for the MusicKit service entitlement, setting the proper Info.plist values, and prompting for permission. However, if the user does have an active subscription, Under Cover takes advantage of that to offer the ability to hear the albums as they're guessing them. To accomplish this, the single player game controller uses the system music player singleton and manages its queue as the game progresses, playing a random song from each album during the guessing stage.

Design Notes

In line with the decision to move quickly, I did not do extensive wireframing or mockups. Instead, I decided to take some time to think through the app's needs, come up with a direction and some starting components, and then proceed to "design in code."

I've found Freeform to be a great tool for this kind of conceptual work (and for organizing thought during development overall.) Having a bucket to collect and draw connections between everything from links to graphic inspiration to API documentation pull quotes to sketches and diagrams and to-do lists works very well for the way I think and helps me keep the big picture in mind throughout.

A screenshot of a Freeform board featuring a messy diagram containing notes on the app's views and data.
Diagramming the app at an early stage. It ain't pretty, and that's partly the point

I designed the data model in parallel with diagramming the user flow, iteratively determining what data really needed to be stored based on what I want the user to see and do.

When it came to settling on an overall visual direction I had a few primary points I was considering:

  • Fun. Games are supposed to feel fun, right? I knew I wouldn't have time to implement the kinds of shiny, maximalist design techniques I usually see used to convey fun. Maybe I could go more for something irreverent or even, dare I dream, funny.
  • Staying out of the way. Since the key interaction in the app is studying an album cover as it slowly de-blurs, I wanted to create a neutral environment that wouldn't fight for attention.
  • “Cool,” or something. It was important for me that it looks cool because I like things that look cool! I don't know why we as designers have decided it would be literal death to ever admit this, but aesthetics "for their own sake" do actually provide value in that humans appreciate aesthetics and it's nice when things are nice. Also, it's a music game. People who like music are cool (or at least want to feel that way.)
  • Quick! Remember, I wanted something I could execute on fast.

So, I needed to do a lot with a little. I wanted something irreverent and immediate, minimal and to the point. What better a jumping off point then than one of the great music genres of all time, punk rock?

A collage of show posters for Siouxie and the Banshees, GBH, Bad Brains and more.

The classic punk rock show poster has a defined aesthetic based on limitations. Black & white, high contrast. Cheaply printed with images rendered in low quality. Lots of literal "whitespace." And they were fun! Even the bad ones were charming in their own way.

This served as a good point of focus. While the end product is in no way meant to literally evoke a show poster, the concept provided some useful constraints and the ingredients for a consistent visual language.

Several iPhone mockups showing different font pairings.
Type tests

For type, after some trial and error, I landed on the pairing of SF Mono for body and PP Nikkei Maru Ultrabold for display. A monospaced font is a perfect fit for the punk poster aesthetic, and Nikkei Maru's Ultrabold's forcefulness is just enough of "too much."

Four photos, each in black and white with duotone treatment and yellow text over top.
At the end of each game the user is given a reward/consolation in the form of a lovely jpeg.

As a further tie-in to the poster idea, I applied halftone printing effect to a collection of photos which show when the user completes a game.

Two Figma mockups showing designs for the category index and round results screens for the app.
That Wayne Shorter album rips, btw.

I drafted a couple sections of the app in Figma to give myself a jumping off point. The screen designs here were mostly unaltered in the shipping product and were extrapolated on for the other sections of the app. The simple pill button style became a key component, as did the short-throw drop shadows to highlight album covers and other featured elements.

A Brief, Practical Introduction to Custom Fonts in SwiftUI

We love Apple's system fonts but sometimes one wants to spread their wings and fly free. Here's a simple guide to using a custom font in your SwiftUI project.

Demo Project

Behold the best albums of last year, objectively. (Project available on my GitHub.)

I'm pretty happy with the layout above. Let's see if we can mess it up a little!

Get a Font

First you need a font file ("cool-font.ttf", "cool-font.otf", etc.). For the purposes of this demo project I'll be using Hatton Bold from the wonderful Pangram Pangram type foundry.

Put the Font in Xcode

Import the font file into your project. (Just drag it in.)

When importing you may be asked if you want to add this file to your app target(s). You do.

You will also need to specify the font in your info.plist. Add the key "Fonts provided by application" and make the first value the name of your font file including its file extension.

Put the Font in a View

To apply this font to a view we just need to use the .font modifier and the Font.custom(_:size:) method, specifying the name of our font without its file extension.

Text("2023")
  .font(.custom("PPHatton-Bold", size: 56))

Put the Font in the Navigation Bar

Applying it to the Navigation Title means we have to interact with a UIKit class, UINavigationBar, at least for now. (I expect a more SwiftUI-ish way to come about at some point.)

.largeTitle and .inline modes are set separately. In our .onAppear block we'll create two fonts and apply them.

struct HomeView: View {
    @State private var top12: [GoodAlbum] = GoodAlbum.top12of2023
    
    var body: some View {
        NavigationStack {
            VStack {
                // Cool stuff...
            }
            .navigationTitle("Top 12 Albums")
            .onAppear {
                let customLargeTitle = UIFont(name: "PPHatton-Bold", size: 48)!
                UINavigationBar.appearance().largeTitleTextAttributes = [.font: customLargeTitle]
                
                let customInline = UIFont(name: "PPHatton-Bold", size: 20)!
                UINavigationBar.appearance().titleTextAttributes = [.font: customInline]
            }
        }
    }
}

If force-unwrapping these UIFonts is wrong, I don't wanna be right.

There are plenty of other properties you can change in these text attributes dictionaries, including things like tracking and kerning. Worth poking around.

Those numerals 🤤

Make it Nicer

If you're like me you're going to change your mind on the font about 50 times. Let's create a quick Font extension so that we only have to write/overwrite the font name in one place.

extension Font {
    static func displayFont(ofSize size: CGFloat) -> Font {
        return Font.custom("PPHatton-Bold", size: size)
    }
}

SwiftUI: Multiple Buttons in a Single List Row

In SwiftUI, we love Lists. We love Lists so much because we simply must love Lists because SwiftUI loves Lists and, really, if you've spent any time in SwiftUI you know you're better off just learning to love what it loves.

So, sometimes we put a lot of stuff in a List. You know, really load it up. Maybe we throw in some of our custom views from elsewhere. And this is the part where I usually get caught up and have to re-realize the same thing again. I'll be tapping or clicking around while testing and notice that one button is triggering the action of another. Buttons within my list are all of a sudden unreliable and fire seemingly random bits of functionality.

That is because a list row itself functions as a tap target, so each button within in it does not intercept its own taps. So, the row instead funnels all those taps to one of the Buttons, seemingly at random (or maybe there's an order to it. Don't @ me, I don't really care.) Luckily the workaround is simple: Just set a button style. Any style, just as long as it's not DefaultButtonStyle.

Here's an example where we have three Buttons in the same List row. Each Button sets a different value to a @State variable which defines the color of elements within another of the List's rows.

import SwiftUI

struct ContentView: View {
    @State private var brainsColor = Color.primary
    
    var body: some View {
        List {
            Section {
                VStack {
                    Image(systemName: "brain.fill")
                        .resizable()
                        .scaledToFit()
                        .frame(maxWidth: .infinity, maxHeight: 100, alignment: .center)
                    Text("BRAINS!")
                        .tracking(2)
                        .font(.custom("Silkscreen-Regular", size: 44))
                }
                .foregroundStyle(brainsColor)
            }
            
            Section("Brain Color") {
                HStack(spacing: 44) {
                    Button {
                        brainsColor = .yellow
                    } label: {
                        Circle()
                            .foregroundStyle(.yellow)
                            .frame(width: 44)
                    }
                    
                    Button {
                        brainsColor = .indigo
                    } label: {
                        Circle()
                            .foregroundStyle(.indigo)
                            .frame(width: 44)
                    }
                    
                    Button {
                        brainsColor = .mint
                    } label: {
                        Circle()
                            .foregroundStyle(.mint)
                            .frame(width: 44)
                    }
                }
                .frame(maxWidth: .infinity, alignment: .center)
            }
        }   
    }
}
An animated gif showing the user interface defines above. There is an SF Symbol for a brain and the text "BRAINS!". There are three buttons to toggle the color of the symbol and text, but as they are clicked only one of the buttons' colors gets applied now matter which is clicked.
Please! My buttons! They're very sick!

As implemented, only one color gets set (the last one) no matter which button you click. However, simply adding an explicit button style to each one allows them to function as expected.

Button {
    brainsColor = .indigo
} label: {
    Circle()
        .foregroundStyle(.indigo)
        .frame(width: 44)
}
.buttonStyle(PlainButtonStyle()) // OK, List, does this make you happy?!?
The same interface from before: Three buttons which toggle the color of a brain image and some text. This time the buttons work!
That's satisfying.

Weird!