Building a UI for data I can't see yet (album-grouping a MusicKit playlist)
This app was later renamed Deep Cut Atlas. It’s called “Discoverer” throughout below, because that’s what it was called on the day this happened.
Here’s a strange spot to be in: I built a whole screen for my Apple Music app this week, ran it, and looked at four albums that aren’t in my library. They’re fake. I can’t see my real “To Check Out” playlist on this machine at all - MusicKit doesn’t run in the simulator, and the real thing needs a paid developer account I haven’t set up. So the question that hung over the whole task was: how do I know the screen is right when I can’t point it at real data?
The answer turned out to be the boring disciplines, done on purpose: a fake data source I control, and tests that pin the behavior. Let me walk through it, because the MusicKit modeling had some sharp edges I didn’t expect.
A playlist is a flat list. I want albums.
The tab is album-centric. You saved some tracks to check out later, and I want to show them grouped into album cards - art, title, artist, year, and a little badge saying whether it’s an album, EP, single, or compilation. But a MusicKit playlist isn’t albums. It’s a flat list of tracks. So step one is grouping.
That sounds trivial until you look at what a playlist track actually carries. MusicKit’s Track is an enum - .song or .musicVideo - but it gives you convenient pass-through properties so you don’t have to unwrap the case every time:
for track in detailed.tracks ?? [] {
let key = (track.albumTitle ?? "") + "\u{1F}" + track.artistName
// group by key, preserving first-appearance order
}
You get track.title, track.artistName, track.albumTitle, track.artwork. What you don’t get is the album’s release date or its type. Those live on Album, not Track. To fill them in properly I’d have to resolve each album from the catalog - one network request per album - and the Apple Music API will rate-limit you if you fire those in a loop. For a small playlist that’s fine; I noted it and moved on rather than over-engineer a batching layer I don’t need yet.
(Small Swift thing I liked: detailed.tracks ?? [] compiles even though tracks is a MusicItemCollection, not an array. The collection is ExpressibleByArrayLiteral, so the empty literal becomes an empty collection. Nice nil handling for free.)
MusicKit will not tell you what an EP is
The badge needs four categories. MusicKit gives you two booleans on Album: isSingle and isCompilation. That’s it. There is no isEP. Apple just doesn’t expose it.
So EP detection is a guess:
if album.isCompilation == true { return .compilation }
if album.isSingle == true { return .single }
if album.title.localizedCaseInsensitiveContains("- EP") { return .ep }
return .album
I’m leaning on the convention that EP titles often end in ”- EP”. It’s a heuristic, and I wrote it down as a thing to validate on a real device, because I genuinely don’t know yet how reliably Apple tags them. I’d rather ship an honest guess with a TODO than pretend I’ve solved it.
The mock is the screen I actually look at
Because none of this runs in the simulator, the development surface is a mock service that returns hardcoded albums - one of each type, on purpose, so the filter chips and the badges all have something to render. The real service and the mock both satisfy the same protocol, and the app picks between them at build time. The view doesn’t know which one it’s talking to.
That’s what let me build the whole thing today. The screenshot I ended up with shows Mordechai (Album), Texas Sun (EP), Numb (Single), and a Bonobo compilation, each with the right colored badge, in playlist order. None of it is real. All of it proves the layout, the grouping, the ordering, and the filter work.
The test that earned its keep
I’d written five new tests for the view model - load, the missing-playlist state, the filter, remove, create. They passed. Then the full suite went red on a test I wrote last week.
I’d bumped the mock playlist from 3 tracks to 5 to get enough albums for the demo. An older test still asserted trackCount == 3. The failure was correct - I’d changed shared fixture data and a previous assertion was now a lie. Thirty seconds to fix, but it’s the whole argument for running the entire suite instead of just your new tests: shared mock data is a dependency, and changing it ripples.
What I’m sitting with
It feels backwards to “finish” a feature you’ve never seen work on real data. But I think the split is honest: the logic and the layout are proven against data I control, and the part I can’t prove yet - does my actual playlist read correctly, do EPs get tagged right - is written down as a device-verification task waiting on a developer account. The mock isn’t a stand-in for testing. It’s the thing that let the feature exist at all before the hardware did.
Related reading
The mirrored window lied about the phone being unlocked
A one-line grouping fix that was already written, and three walls between it and proof: actor isolation, log stream's Mac-only scope, and a mirrored session that looks unlocked when the device isn't.
Pre-release albums, and the fix I couldn't fully verify
Apple's 'Track N' placeholders rendered as real data. The one detection signal I couldn't confirm became the one signal I stopped depending on.
A subscribe button, a MusicKit API I'd never used, and a sheet that dismisses into silence
musicSubscriptionOffer presents Apple's native sheet - and nothing in the app would ever notice it closing. The correctly-coded action that was still a functional dead end.