I recovered a stashed fix, then a live repro on my phone showed me a different bug
The Playlist tab in Deep Cut Atlas had an intermittent bug: sometimes, on launch, it would get stuck on “Loading playlist…” forever. No error, no retry button, nothing to do but force-quit. A previous session had already diagnosed and fixed it, tests passing and everything, then paused mid-verification and stashed the work. Today I picked it back up, and it turned into two separate lessons.
Recovering the fix
Applying a two-day-old stash against a codebase that’s had several commits land since is exactly the kind of thing I expect to be painful. It wasn’t. git stash apply did a clean three-way merge across all three touched files, including one file I’d modified for an unrelated fix earlier the same day. Worth remembering: a stash isn’t a snapshot frozen against one specific commit, it merges the same way a branch does.
A linter that can’t see what you can see
The fix itself adds an in-flight request de-duplication guard, so two concurrent callers asking for the same playlist’s contents share one request instead of racing two. SwiftLint immediately flagged it: “errors thrown inside this task are not handled.” Except they are. The result is captured, stored, and awaited two lines later with proper error propagation. I compared it to two other functions in the same file doing the identical pattern that weren’t flagged, and the difference was academic: those use Task.detached, this one uses a plain Task { }. The rule apparently pattern-matches on the initializer syntax rather than tracing whether the result actually gets consumed. I could have restructured the code to dodge it, but that would have meant claiming a concurrency benefit (running detached, off the main actor) that doesn’t even apply here. A scoped, explained disable comment was the more honest fix.
The live repro that wasn’t the bug I was looking for
Here’s the part that actually mattered. Scripted verification wasn’t available for this one - my device UI automation path was down (a WebDriverAgent dependency timed out, unrelated to this issue), and scripted device launches didn’t surface the app’s console output either. So I did the phone-in-hand pass myself.
What I saw: stuck. But not just Playlist. History showed no album artwork. Discover was stuck on its own loading state. All three, at once.
My first instinct was mild panic, did the fix not work? But the bug I’d fixed was narrow and specific: a race between two particular async calls both wanting the same playlist’s data. It never touched Discover’s code at all. Something stuck across three unrelated tabs simultaneously pointed at a different, bigger problem, not a refutation of the narrow one.
A few checks ruled things out fast. The app process was still alive, not crashed. Wifi was fine. The real Apple Music app worked perfectly on the same phone, same network, same moment. And no hang notification fired, which turned out to be its own useful clue rather than a dead end: iOS’s hang detector specifically watches the main thread, so its silence suggested the UI itself wasn’t frozen, something async was just never finishing.
I don’t have a stack trace. Getting one needs Xcode’s debugger attached live. So rather than guess at a root cause I can’t see, I split it into its own investigation and shipped the fix I could actually verify on its own terms: tested, code-reviewed, pattern-matched against an identical mechanism already running in production elsewhere in the same file. The bug that showed up on the phone is real and needs real profiling, not a guess bolted onto an unrelated fix because it happened to show up during the same testing session.
What’s next
The narrow fix is merged. The broader stall (all three tabs, no crash, no hang notification, real Apple Music unaffected) is its own open investigation now, waiting on an Instruments session or a live-attached debugger to actually see where the async work is getting stuck.
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.
A tracklist section, and why it took 30 minutes
One protocol method, a reused state enum, a routing convention that held, and a lint budget that forced a split worth making anyway.
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.