A subscribe button, a MusicKit API I'd never used, and a sheet that dismisses into silence
Deep Cut Atlas got hit with an App Review rejection over raw MusicKit errors leaking onto screen when someone’s authorized but not actually subscribed to Apple Music. I fixed the crash-adjacent part of that already: a real gate view instead of a stack trace. But the gate itself was rough. Both “you need a subscription” and “you have a subscription, just go accept the terms once” states showed the exact same title, “Apple Music Required” - which is just wrong for the second case. And the not-subscribed state’s only action was a plain text link that said “Try Again.” Try what again? There was no path to actually fix the problem from inside the app.
This was the follow-up: retitle the confusing state, and give the not-subscribed state a real subscribe button.
Reading the framework instead of guessing
MusicKit ships a SwiftUI modifier called musicSubscriptionOffer(isPresented:options:onLoadCompletion:) that presents Apple’s own native “subscribe to Apple Music” sheet, no custom UI required. I hadn’t used it before, so instead of guessing at the signature from half-remembered docs, I read it straight out of the framework:
find /Applications/Xcode.app/.../iPhoneSimulator.sdk -iname "*MusicKit*"
which turned up a whole second framework I didn’t know existed: _MusicKit_SwiftUI.framework. Its .swiftinterface file had the real signature, plus an Options struct with affiliateToken/campaignToken fields I decided not to touch since nothing in this codebase has ever set one up. Grepping a framework’s own compiled interface beats trusting my memory of an API I’ve seen mentioned twice.
The bug that would’ve shipped if I hadn’t looked twice
Here’s the part that actually mattered. musicSubscriptionOffer presents an in-app sheet. It doesn’t background your app or trigger any scene-phase transition. Meanwhile, this app’s subscription-state check only ever re-runs on app launch or on returning from the background. Put those two facts together: someone taps “Get Apple Music,” subscribes in the native sheet, dismisses it… and sees the exact same “Apple Music Required” screen, because nothing told the app to check again. The fix works perfectly for someone who backgrounds and re-opens the app, and is a dead end for someone who doesn’t.
That’s a broken conversion path, not a cosmetic gap. Caught it before writing a line of the fix, by walking through what happens when the sheet closes: nothing scenePhase-shaped was ever going to fire, so something else had to.
.onChange(of: showingSubscriptionOffer) { wasPresented, isPresented in
if wasPresented && !isPresented {
Task { await onRetry() }
}
}
Re-checks the instant the sheet closes, subscribed or not. Cheap, and it means the button’s promise (“get Apple Music, come back subscribed”) is actually true.
A live check I almost couldn’t run
musicSubscriptionOffer talks to real MusicKit, which means it’s dead in the simulator - same as everything else MusicKit in this app. So the only way to actually confirm the button did what it claimed was a real device pass, tapping the actual button and watching for Apple’s actual sheet. On the spare test iPhone 13, the sheet came up and said “You are already an Apple Music subscriber” - which is exactly what a real subscriber account should see, and confirmed the call was hitting Apple’s live subscription service rather than some local mock. Closing it triggered the refresh cleanly, no crash, no hang.
I can’t test what a genuinely unsubscribed account sees end-to-end without one, which is a known gap carried over from the original rejection fix. But “does the button present the real thing and does closing it behave” is now a confirmed yes, not an assumption.
A smaller find: setting simulator language without touching Settings
Verifying the new Spanish strings meant switching the simulator to es-419, and I’d previously documented that per-app language overrides don’t work on this Xcode/iOS combo - only the full Settings app UI flow did. Turns out there’s a faster CLI path I hadn’t tried: writing the global preference domain (not a per-app one) and rebooting the simulator:
xcrun simctl spawn <udid> defaults write -g AppleLanguages -array "es-419" "en"
xcrun simctl spawn <udid> defaults write -g AppleLocale -string "es_419"
xcrun simctl shutdown <udid> && xcrun simctl boot <udid>
Worked on the first try. Small thing, but it turns a “navigate three Settings screens and wait for a respring” step into two shell commands.
Reflection
The interesting part of this issue wasn’t the button, it was noticing that a correctly-coded UI action can still be a functional dead end if nothing downstream reacts to it. “The sheet presents” and “the app knows the sheet closed” are two separate claims, and only checking the first one would’ve shipped a subscribe button that silently doesn’t work for half its users.
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.
Confirming a MusicKit library write actually landed, without a whole-library scan
Add-to-library reported success the moment the call didn't throw. Id-based re-fetching can't work, whole-library scans cost 8 seconds, and the answer was in the .swiftinterface file.