There is a tempting way to think about an app capability: it is a door you might want open later, so you leave it unlocked now, just in case. That instinct is exactly backwards. On iOS, every capability you declare is not a convenience you have banked — it is a claim you have made about what your app does, and a reviewer will hold you to it. Declare background audio and you have promised, in a config file, that your app keeps playing sound after the user swipes away. If your app then does the opposite, you have not left a door open for later; you have written down a lie about yourself and handed it to the one reader who checks. We know because we did precisely that — and then, having fixed it, did it again.
A promise you make in a config file
The mechanism is small and unglamorous. An iOS
app ships a property list, Info.plist, that among other
things can carry a key called UIBackgroundModes — an
array of the specific things the app is allowed to keep doing while it is
not on screen: playing audio, tracking location, handling voice-over-IP,
finishing a download. The audio mode is the one that matters
here. It exists for the apps that genuinely need it: music players,
podcast clients, streaming services — software whose entire job is
to keep sound going while you do something else. Declaring it tells the
operating system, and Apple’s reviewer, “this app is one of
those.”
Our games are not one of those. A puzzle game
and an arcade game have no reason to make noise once you have left them;
the respectful, battery-friendly thing — and the thing our code
actually does — is to cut all audio the moment focus is lost. In
Flutter that is a handful of lines: a WidgetsBindingObserver
listens for didChangeAppLifecycleState, and the instant the
state goes to paused, we pause every sound. That behaviour is
deliberate and we are proud of it — it is the whole subject of an
earlier devlog on why a
well-behaved game goes silent the moment you leave it. Which is what
makes the UIBackgroundModes declaration so absurd in
hindsight: the capability we had asked for was one our own code goes out
of its way to refuse to use.
We shipped the same mistake twice
Here is the part that is not flattering, told
straight. On 2026-05-22 our Sudoku game was rejected under App Store
Review Guideline 2.5.4 — the rule that an app must not declare
background modes it does not actually use. The cause was exactly the
UIBackgroundModes: [audio] entry described above, sitting in
an Info.plist for an app that pauses its audio on background.
We removed the key, resubmitted, and the app went through. Problem solved,
we thought. It was not, because we solved the wrong problem.
We fixed the file and never wrote down
the lesson. There was no note in a shared place that said
“a game must never declare background audio,” no check that
would catch it, nothing but the memory of the one person who happened to
deal with the rejection. Memory is not a control. So on 2026-07-09 our
tank game — a completely separate app, built and configured on its
own timeline — was rejected under the identical guideline for the
identical reason: the same audio background mode, declared in
a game that also goes silent when you leave it. Two apps, one defect, two
rejections, roughly seven weeks apart. The second one stings more than the
first, because the first was a mistake and the second was a mistake we had
already been warned about and let happen anyway.
The fix was deletion, not configuration
The immediate fix is almost anticlimactic:
you delete the key. A default Flutter app ships without any
UIBackgroundModes entry at all, which is correct, because the
default app has no background feature. The risk is never that the key is
missing and should be there; it is always that someone added it
— copied from a tutorial, inherited from a template, pasted from a
Stack Overflow answer meant for a media app — and it quietly rode
along. An empty <array/> under the key is no better,
by the way: it still draws a reviewer’s eye and invites the same
question. The right state is not “declared and empty.” It is
“not declared.”
This is the principle of least privilege wearing ordinary clothes. Security people say it about access rights: grant the narrowest set of permissions the job actually needs, because every extra grant is attack surface you now have to defend. A store capability is the same shape of thing. Every mode you declare is a promise you have to keep, a behaviour a reviewer can test you against, and a line of divergence between what your manifest says and what your binary does. The capability you are not using is not sitting there harmlessly — it is the single most likely thing to get your next release bounced, precisely because nobody remembers it is there.
The real bug was in our memory, so we fixed that
Deleting the key fixes one build. It does
nothing to stop a third app, a year from now, from reintroducing the exact
same entry — and the two-time recurrence is the actual story here,
not the plist. A fix that lives only in one engineer’s head is not a
fix; it is a countdown. So the durable change was not in any app’s
code. We wrote a small audit script that scans every app’s
Info.plist and fails loudly if it finds a
UIBackgroundModes entry — including an empty one —
that is not accompanied by an explicit, written waiver. It runs before any
iOS build and as part of release clearance, so a rogue capability cannot
reach a reviewer without a human first having typed out, on the line above
it, exactly which background feature justifies it and why.
The waiver mechanism matters as much as the block. We are not banning background audio forever; some future app might genuinely stream sound and legitimately need the mode. What we are banning is the silent declaration — the capability nobody chose on purpose. To keep the mode now, you have to leave a marker comment stating the real feature and be ready to show it, which turns an accidental liability into a deliberate, documented decision. That is the difference between a rule and a habit: a habit is what one person remembers, and a rule is what the build refuses to skip. We had relied on a habit twice and it failed twice; the third time, the machine checks. The same instinct runs through our automated QA gate — the things you cannot afford to forget should not depend on remembering them.
If you want to see the behaviour this whole episode was contradicting — the game cutting its own sound the instant you look away — it is running in Iron Swarm and in our Sudoku app, both of which now declare exactly the capabilities they use and not one more. That is the quiet payoff: the apps do less on paper than they used to, and are more honest for it.
The lesson generalises past this one key. A manifest, an entitlement, a scope on a token, a permission in a settings screen — every one of them is a claim you are making about your own software, and the ones you are not using are the ones that will surprise you, because you have forgotten you are still making them. Declare what you use. Delete what you don’t. And when a fix depends on a person remembering, the fix is not finished — the longer version of how the small disciplines behind a shipped product compound is on the WizusLabs Engineering blog. A permission you do not use is not a convenience you have saved; it is a debt you have forgotten you owe.
Notes
This is a first-hand engineering account of
two App Store rejections we caused and the change we made in response. It
is iOS-specific by nature — the mechanism is Apple’s
Info.plist, the UIBackgroundModes key, and App
Store review — and nothing here is a claim about any other platform.
It deliberately carries no invented metrics: the only figures in
the piece are two rejection dates from our own records (Sudoku,
2026-05-22; the tank game, 2026-07-09) and the fact that it happened
twice. We have not attached a review-time, revenue, or download number to
any of it, because we did not measure those and manufacturing one would
undercut the point. The specifics of what App Store review requires can
change over time; the guideline text and the property-list documentation
we relied on are cited below, and you should verify current wording
against Apple’s own pages before relying on it.
Sources
The Apple requirements this post leans on are documented by Apple (living documents; accessed 2026-07-23):
- Apple — App Store Review Guidelines, §2.5.4 (Software Requirements): an app may declare support for a background mode only if it provides functionality that genuinely requires persistent background execution; declaring a background mode the app does not actually use is grounds for rejection.
- Apple Developer Documentation —
UIBackgroundModes(Information Property List): theInfo.plistkey that enumerates the specific background services an app requires, including theaudiomode reserved for apps that play or record audible content in the background.
Keep reading: all posts on the WizusLabs Engineering blog.