WizusLabs Engineering · Craft

Why a well-behaved game goes silent the moment you leave it

A well-made mobile game does something that can look almost rude: the instant you swipe away, its sound cuts out completely. That is not a bug or a limitation. It is the single most respectful thing the app does — and getting it wrong is how a game gets rejected from the App Store twice.

By WizusLabs Engineering · 2026-07-20 · ~7 min read

There are only two ways a game can behave when you swipe it away mid-level. It can keep playing — the menu loop still humming while you check a message, the app you eventually have to hunt down and force-quit because it will not shut up — or it can go silent, instantly and completely, the moment it loses your attention. We build for the second, on purpose, and we also refuse to tell the App Store our game is the kind that needs to keep playing. This is a first-hand account of why that silence is the polite thing to do, why claiming otherwise breaks the rules, and how we managed to break the same rule twice before the fix finally stuck.

The app that keeps singing after you’ve left the room

Picture the badly-behaved version. You open a game, a cheerful loop starts, and then a call comes in — or you switch to a podcast, or your maps, or you just lock the phone and drop it in your pocket. A well-made app ducks out of the way. A careless one keeps its music running underneath everything, fighting your podcast for the same speaker, spending battery to render sound nobody is listening to, and turning up in the Now Playing controls as though it were a media player. It is the digital equivalent of a house guest who keeps talking after you have walked out of the room.

Three concrete costs sit behind that annoyance. The first is battery: audible playback keeps the audio hardware and a slice of the CPU awake, and a game that plays to an empty room is spending power for nothing. The second is the shared audio session — a phone has one set of speakers, and every app is a guest at that table; a game that will not yield its seat is stepping on whatever you actually chose to hear. The third is plain respect for attention: you left, and the app should take the hint. None of this is exotic. It is ordinary courtesy, encoded — and, as it turns out, also the law of the platform.

The one callback that makes it well-behaved

The mechanism is almost embarrassingly small. Every app moves through a handful of lifecycle states — roughly, it is resumed and in the foreground, inactive while it is losing or regaining focus, hidden, or fully paused in the background. In Flutter, the framework hands you one callback that fires on every one of those transitions: a WidgetsBindingObserver with its didChangeAppLifecycleState method. Register the observer once, and the operating system tells you the exact instant your app stops being the thing the player is looking at.

So the whole rule is this: when that callback reports anything other than resumed, pause the audio; when it reports resumed again, bring it back. We pause on the first sign of lost focus rather than waiting for the full background transition, because the honest goal is silence the moment attention leaves, not silence eventually. It is a few lines, it lives in one place, and it is the entire difference between a game that behaves and one that does not. The craft of which sounds are worth playing in the first place is a separate story — we wrote that up in designing game audio that players don’t mute — but pausing on focus loss is the same reflex we lean on to survive interruption generally: the app should always assume it is about to be interrupted. Getting the sound to stop is easy. The interesting part is what you are not allowed to tell the platform.

The capability we must not claim

Here is where a courtesy hardens into a rule. iOS lets an app declare, in its configuration, that it needs to keep running in the background for a specific reason — and one of those reasons is audio. A music player, a podcast app, an audiobook reader: these genuinely need to keep playing while you do something else, so they declare the background-audio capability and the system keeps them alive. The declaration is a promise about what the app is for.

A game is not for that. Our games pause on background — that is the whole of the previous section — so they never make a single audible sound while backgrounded. Declaring the background-audio capability would therefore be two kinds of wrong at once. It would be illegitimate, claiming a purpose the app does not have; and it would be self-contradicting, because the app’s own lifecycle code is busy silencing the very audio the capability insists it needs to keep playing. Apple’s reviewers treat this as a submission blocker under the guideline that says background modes may be used only for their advertised purpose. It is the kind of platform-specific rule that makes shipping one codebase to two stores less single than it sounds. And a default Flutter app ships without this key for exactly this reason — the risk is never that you forgot to add it; the risk is that someone adds it, thinking it will help the sound work, when the sound works fine without it.

We got it wrong. Then we got it wrong again.

We know the reviewers treat it that way because they told us — twice. In May 2026, our Sudoku app was rejected under that guideline for declaring the background-audio capability it did not use. We removed the declaration, the app went through, and we left a comment in the configuration file noting the removal. Fixed. Except a fix that lives only as a comment in one project’s file, and as a memory from one afternoon, is not really fixed. Seven weeks later, in July 2026, our tank game shipped the identical defect — the same capability, declared for the same non-reason — and was rejected under the same guideline. Same mistake, same rule, second app, second rejection.

That second rejection is the interesting one, because it was our fault in a way the first was not. The first time, we did not know. The second time we had known, and had let the knowledge evaporate. There is no clever engineering story here to make us look good; the story is that a lesson learned by one person, on one project, on one day, is a lesson the studio has not actually learned. That is the failure mode worth writing down — and worth building a wall against.

What a game’s audio does across the app lifecycle Four lifecycle states in sequence: resumed in the foreground, inactive as focus is lost, paused or hidden while backgrounded, and resumed again on return. Audio plays only while the app is resumed and in the foreground; it is paused across the inactive, hidden, and paused states, and resumes when the app returns to the foreground. The lifecycle callback fires on every transition. resumed in foreground inactive losing focus paused / hidden backgrounded resumed back in view Audio playing Audio paused — from the first sign of lost focus Audio resumes WidgetsBindingObserver.didChangeAppLifecycleState fires on every transition
Sound follows focus. Audio plays only while the app is resumed and in the foreground; the instant focus is lost it pauses, and it never plays while backgrounded — which is exactly why declaring a background-audio capability would contradict the app’s own behaviour.

A fix that outlives the person who remembers

So the real fix was not editing a configuration file a third time. It was making the rule impossible to forget. We wrote it down as a workspace-wide mandate — no iOS app in the portfolio may declare a background mode it does not genuinely use — and, more importantly, we made a machine enforce it. A small audit script now scans every app’s iOS configuration for a live background-mode declaration, ignores the documented-removal comments so that a note about not having one never trips it, and exits with an error if it finds an unwaived entry. It runs before every iOS build and as part of release clearance; a hit stops the build. A companion listing mode prints an inventory of every app’s state without ever failing, so we can see the whole portfolio at a glance.

The script is strict, not absolute, because absolutism is its own kind of dishonesty — some future app of ours genuinely might play audio in the background, and a gate that cannot express ‘yes, on purpose’ just gets switched off the first time it is inconvenient. So there is one narrow escape hatch: a waiver. To keep a background mode, you place a specific marker comment beside the declaration naming the mode and the real reason, and you record it in the changelog. No marker, no exceptions — the build fails. The waiver is deliberately annoying to add, because it should be: it forces a human to state, in writing, the genuine background feature that justifies the claim. A capability you cannot explain in one comment line is a capability you should not be declaring.

There is a general lesson buried in the specific one. The first rejection taught us a fact. The second taught us that facts do not stay taught — that the only knowledge a studio really owns is the knowledge it has encoded into something that runs without being remembered. A comment is a note to a future reader who may never arrive. A script is a rule that shows up uninvited, every build, and refuses to be polite about it. We did not need to be smarter the third time. We needed to stop relying on being smart at all.

The through-line is smaller than it looks. A game going silent the instant you leave it is not the platform being fussy or the framework being limited; it is the app agreeing that your attention is the thing that matters, and stepping aside the moment it no longer has it. Claiming otherwise — telling the store your game needs to keep playing when it does not — is not a shortcut to better sound; it is a promise you immediately break in your own code. We learned that the boring way, twice, which is why the rule now lives in a script instead of a memory. The most respectful thing our audio does is stop. The best thing we did about it was make sure we can never again forget why. If you want the rest of how we build, it is all on the WizusLabs Engineering blog.

Notes

This is a first-hand devlog about our own apps, not a general iOS tutorial. The specifics — that our Sudoku app was rejected in May 2026 and our tank game in July 2026 under the same background-audio guideline, and that the fix is now a local audit script with a comment-based waiver mechanism gating every iOS build — are our own history and our own tooling decisions, recorded in our workspace notes, not public figures. The post deliberately contains no metrics: we are not going to quote a battery-drain number or a percentage of phones kept on silent, because we have not measured those on the device you happen to be holding, and inventing them would undercut the one thing this post is actually about. The lifecycle-state names and the callback belong to the Flutter framework and are cited below; everything framed as a studio decision is exactly that.

Sources

The public rules and APIs this post leans on (living documents; accessed 2026-07-20):

Keep reading: all posts on the WizusLabs Engineering blog.

← Back to the Blog