A tracklist section, and why it took 30 minutes
Today’s feature was small: the Discover tab’s release sheet should show the album’s tracks, so you can judge a release before adding it to your playlist or dismissing it. What made it worth writing about is how little new thinking it needed.
The change
The sheet needed track data the app didn’t fetch anywhere yet. One new read on the service protocol:
func fetchTracks(forCatalogAlbum album: LibraryAlbum) async throws -> [LibraryTrack]
The real implementation is a single catalog request with the .tracks relationship, mapped through a track mapper that already existed for the playlist flow. The mock returns per-album fixtures and gets a failure-injection case like every other method, so tests can fail exactly this call and nothing else.
The view model got a loadTracks() and a state property. I didn’t define a new state enum, because the codebase already has one for exactly this shape of problem: SuggestionsState (loading, loaded, empty, failed) drives the “more from this artist” sections in two other sheets. Reusing it meant the tracklist’s loading spinner, empty copy, and failed-with-retry row came out consistent with the rest of the app without any design decisions.
One routing decision worth noting. This sheet’s two existing actions (add to playlist, not interested) delegate to the parent Discover view model, because they mutate the feed and have to stay consistent with it. The tracklist read goes straight to the service instead. That split, reads direct and feed-mutating writes through the parent, is the convention the sheet was already built on. Following it kept the parent untouched except for one line in the factory method.
The lint budget did its job
The service class was sitting at exactly 300 lines of code, the SwiftLint type-body limit, after the performance work. Adding a 7-line method meant something had to give. The three static DTO mappers moved into a private extension in the same file, which keeps private access working (Swift’s private is file-scoped for same-file extensions) and dropped the class body well under the limit.
I used to find these limits annoying. This one forced a split I should have made anyway: the mappers are pure functions that were never really part of the class’s job.
Verification
186 tests pass, three of them new: tracks load in order, an album with no tracklist shows the empty state, and an injected failure shows the error then succeeds on retry. Then the usual device pass, because MusicKit returns nothing in the simulator: built to the phone over devicectl, opened a few releases, watched real tracklists fill in.
The whole thing was seven files and 160 lines, most of them tests and fixtures. When a feature costs this little, it’s usually not because the feature was trivial. It’s because the last ten features left conventions behind that this one could ride on.
Related reading
I only fixed the screenshot I was asked about, not the ones that were also broken
One recaptured marketing shot looked done - until the deflating question: is this really every screenshot? The other three in the same set were stale too, each in a different way.
The App Store screenshots nobody updated for two weeks
Two folders that look interchangeable and aren't, a JSON cache that survives reinstall, and three rounds of 'looks done' that weren't.
What an accessibility audit found in my own SwiftUI badges
Same-hue text on its own 18%-opacity tint measures at 1.27:1 no matter which color you pick - and the one badge that passed by going bold instead of changing color.