The Five Days Between the iOS 27 RC and Release — I Check Init Order and Entitlement Restore Before Layout
A record of how I narrow the agent's search scope during the short window before an OS release — by launch path rather than by screen. Includes the audit script I run to catch consent and ads init ordering, entitlement restore gaps, and hardcoded size branches.
The RC lands September 9, the release September 14. With the dates finally fixed, I sat down at my iMac and sketched out the plan for the four iOS apps I run as an indie developer. That leaves five days, give or take.
I spent the last cycle's window badly. Almost all of it went into fitting layouts to a new screen size, and it took several days after shipping to understand that layout was not the thing that needed rework.
The consent prompt for tracking sat after the ads SDK initialization. Every screen rendered beautifully on every device. What did not render beautifully was the revenue, because the SDK started without a consent state to carry.
A broken layout can be fixed after release. A broken launch path takes effect before anyone notices you fixed it.
Since drawing that line, I pin the agent's search scope to the launch path rather than the screen whenever an OS turns over. Here is the procedure, and the audit script I keep running.
What I dropped first
Of the five days, RC day disappears into updating simulators and test devices. I want the day before release free for submission checks. That leaves roughly three days of actual work.
Three days will not hold a full visual pass across four apps. So the first thing I decided was not what to include, but what to leave out.
Visual polish on new devices: out. A follow-up release covers it.
Regression checks on existing devices: out. Those passed on the previous build and the OS change does not touch them.
Copy and translation review: out. Store-side updates catch up later.
What stayed was two things only: everything that runs between launch and the first drawn frame, and whether a purchased entitlement comes back. Both are hard for users to see and hard for me to notice.
The failures clustered in the launch path, not the layout
When I listed the places that actually needed rework after an OS turnover, they all sat on the launch path.
Area
How it shows up
How I found out
Consent prompt vs. ads SDK init order
Rendering is fine. Only the revenue rate drops
Weekly report comparison
Restoring a purchased entitlement
Ad-free state never returns. Mail arrives days later
A message from a user
Synchronous reads during launch
Leaves an impression of slowness that no metric captures
The tone of new reviews
Missing usage description keys
The permission prompt never appears and the feature quietly stops
Review feedback, or silence
None of these present themselves as breakage. A crash would be easy. A wrong launch path keeps looking like it works while it eats away at revenue and goodwill.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You will be able to scope what you hand an agent before an OS release by launch path rather than by layout files
✦You will catch a reordered initialization from your own scan instead of hearing about it from users weeks later
✦You will know exactly which checks still need a physical device, so the short window goes to the things nothing else can cover
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Scoping the agent by execution order, not by file type
Ask an Antigravity agent to "support the new device sizes" and it walks straight to the layout files. That is a reasonable reading of the request, and those files really can be fixed there.
But the place I want eyes on is elsewhere. So I began describing the entry point by execution order rather than by kind of file.
<!-- .antigravity/rules/os-update-window.md --># Search scope for the OS update windowLimit the scope to "the path executed from app launch until the first frame is drawn."Read only these three kinds of files:1. Files containing `application(_:didFinishLaunchingWithOptions:)` or `@main`2. Initialization helpers called synchronously from the above3. `Info.plist` and `*.entitlements`Do not read layout definitions, assets, or localized strings unless explicitly asked.Reporting format:- Before proposing any change, output the resolved execution order first- Where the order cannot be determined, say "undetermined" and do not guess an ordering
The third instruction turned out to matter most. Before I added the line about not guessing, the agent would produce a plausible-looking order for initializers that lived in separate files.
Order is genuinely hard to resolve, even by hand. Being told it cannot be resolved narrows down where I need to look myself.
Finding the launch path mechanically
I like to form my own hunch before handing anything to an agent, so a short audit script sits in each project. All it does is find files containing the launch entry point and compare where order-sensitive calls appear inside them.
#!/usr/bin/env python3"""Launch-path audit - only the ordering and entitlement surface that matters before an OS update."""import os, re, sys, json# Calls whose order carries meaning. We want them to appear left to right.ORDERED_CALLS = [ ("att_request", r"requestTrackingAuthorization"), ("ads_start", r"(MobileAds|GADMobileAds)[^\n]{0,40}\.(start|startWithCompletionHandler)"),]# Synchronous work that tends to stall right after launchBLOCKING_IN_LAUNCH = r"(Data\(contentsOf:|contentsOfFile:|semaphore\.wait|DispatchSemaphore)"# Traces of the older purchase API that never moved to current entitlementsLEGACY_STOREKIT = r"(SKPaymentQueue|SKPayment\b|restoreCompletedTransactions)"# Hardcoded size branches - only comparisons against known point valuesDEVICE_POINTS = {375, 390, 393, 402, 414, 420, 428, 430, 440, 812, 844, 852, 874, 896, 912, 926, 932, 956}HARDCODED_SIZE = re.compile(r"[=<>!]=?\s*(\d{3})(?:\.0)?\b|\b(\d{3})(?:\.0)?\s*[=<>!]=")LAUNCH_ENTRY = r"(didFinishLaunchingWithOptions|@main|applicationDidFinishLaunching)"USAGE_KEYS = [ "NSUserTrackingUsageDescription", "NSPhotoLibraryAddUsageDescription", "NSLocalNetworkUsageDescription",]def scan(root): report = {"launch_files": [], "order": [], "blocking": [], "legacy_storekit": [], "hardcoded_size": [], "missing_usage_keys": []} for dirpath, dirnames, filenames in os.walk(root): dirnames[:] = [d for d in dirnames if d not in {".git", "Pods", "build", "DerivedData"}] for fn in filenames: p = os.path.join(dirpath, fn) if fn.endswith((".swift", ".m", ".mm", ".h")): try: src = open(p, encoding="utf-8", errors="ignore").read() except OSError: continue rel = os.path.relpath(p, root) if re.search(LAUNCH_ENTRY, src): report["launch_files"].append(rel) pos = {} for name, pat in ORDERED_CALLS: m = re.search(pat, src) if m: pos[name] = m.start() # Judge only when both land in the same file; otherwise stay silent if len(pos) == len(ORDERED_CALLS): seq = [k for k, _ in ORDERED_CALLS] if [pos[k] for k in seq] != sorted(pos[k] for k in seq): report["order"].append({"file": rel, "found": pos}) for m in re.finditer(BLOCKING_IN_LAUNCH, src): report["blocking"].append( {"file": rel, "line": src[:m.start()].count("\n") + 1, "hit": m.group(0)}) for m in re.finditer(LEGACY_STOREKIT, src): report["legacy_storekit"].append( {"file": rel, "line": src[:m.start()].count("\n") + 1, "hit": m.group(0)}) for m in HARDCODED_SIZE.finditer(src): value = int(m.group(1) or m.group(2)) if value in DEVICE_POINTS: report["hardcoded_size"].append( {"file": rel, "line": src[:m.start()].count("\n") + 1, "value": value}) elif fn == "Info.plist": try: plist = open(p, encoding="utf-8", errors="ignore").read() except OSError: continue rel = os.path.relpath(p, root) missing = [k for k in USAGE_KEYS if k not in plist] if missing: report["missing_usage_keys"].append({"file": rel, "missing": missing}) return reportif __name__ == "__main__": root = sys.argv[1] if len(sys.argv) > 1 else "." r = scan(root) print(json.dumps(r, ensure_ascii=False, indent=2)) # Only ordering violations and launch-time blocking exit non-zero sys.exit(1 if (r["order"] or r["blocking"]) else 0)
Run it against a project where the consent prompt sits after the ads SDK and you get output like this.
The offset for att_request being larger than ads_start is the whole signal. That narrows the work to one file, which is exactly the point at which handing it to the agent becomes useful.
Staying quiet when the answer is undetermined
The part of this script that took me longest was not the detection. It was deciding when not to detect.
When the consent request and the SDK initialization live in different files, textual order and execution order stop agreeing. Put a launch-time registration step in between and the written order can be exactly backwards.
I tried resolving call sites first. That did not work. Branching and lazy initialization push you past what static reading can follow surprisingly quickly.
So I settled on judging only when both calls appear in the same file, and saying nothing otherwise. Ring the bell only for what is certainly wrong; send the merely suspicious to a human. After that concession, I started reading the output all the way to the end again.
An audit that warns a lot stops being read. An audit that stops being read is the same as no audit.
How far entitlement restore goes without a device
I scoped the second surviving item, restoring purchased entitlements, the same way.
What surprised me was how much of it I had assumed needed a physical device and did not. The list that genuinely required one turned out to be short.
What I want to confirm
Device needed
Alternative
Screen transitions after a successful restore
No
Set the owned state in the StoreKit configuration file
The display when restore fails
No
Inject the failure from the same configuration
Repurchase after expiry
No
Run with a shortened renewal interval
The actual order of the consent prompt
Yes
None. Only a true first launch shows it
A completed real purchase
Yes
None. One pass through a sandbox account
Building that table freed up roughly half a day of device time. I moved it to first-launch checks on the wallpaper app, which ships more often than the others.
A consent prompt only happens once per device. There are ways to reset permissions from settings, but the result is not always identical to a genuine first launch, so this is where I keep the thickest slice of device time.
Fold the size branches instead of adding to them
For a while I added one more branch per device generation. Past a couple of dozen, finding the branches I had missed started costing more than adding new ones.
The hardcoded_size section exists to count them. When the count comes back, I ask whether the branch can be folded before I add another.
Express vertical spacing relative to the safe area instead of the screen
Replace per-device branches with a "does it fit" test rather than a height comparison
Collect whatever branches genuinely remain into a single definition file
Here is the ordering I use. Dates shift; the sequence does not.
Day
Work
Done when
1
Update simulators and test devices, run the audit across all four apps
The ordering and blocking lists exist
2
Fix the launch path. Make the agent report execution order before proposing edits
The audit exits with code 0
3
Walk entitlement restore in the simulator
Restore, failure, and repurchase all pass
4
One first launch and one real purchase on a device
The consent prompt order has been seen with my own eyes
5
Submission checks and staged rollout setup
The post-release thresholds are written down
I keep the staged rollout thresholds identical every time: open at 5%, then 25%, 50%, 100%, checking only that crash-free users stay at or above 99.7% and the unresponsive rate stays under 0.20% at each step. Re-deciding the numbers each release lets mood into the judgment.
I would also recommend fixing how many times you run the audit. I run it twice and only twice: once across all four apps on day one, once after the day-two fixes. Without a fixed count, getting exit code 0 slowly becomes the goal instead of the signal.
If you have the same window ahead of you, replace ORDERED_CALLS with the two order-sensitive calls in your own app and run the script against a single project.
If nothing fires, that is still a result. Knowing the ordering is clean is what turns "spend the rest of the week on layout" from a guess into a decision.
I may be missing paths this audit still cannot see, and I keep adding to it. Even so, I panic less after release than I did back when I started from the screen. Thank you for reading.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.