ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-05-26Intermediate

Unifying In-App Review Prompts Across 5 Apps with Antigravity Editor — A Few Days of Notes

Notes from a few days spent unifying SKStoreReviewController trigger conditions across five iOS wallpaper apps I run as an indie developer, using Antigravity Editor's multi-file editing to bring the logic onto one shared coordinator.

SKStoreReviewControlleriOS27Antigravity338in-app reviewindie developer11app-dev49

The trigger was a small uneasy feeling I had late one night looking at App Store Connect. I run five wallpaper apps in the same family, yet the way reviews accumulated and the average ratings were drifting in noticeably different directions. The apps themselves were not that different in quality, so the culprit had to be somewhere in the prompt logic. I opened Antigravity Editor and started untangling it.

I have been publishing apps as an indie developer since 2014, and the cumulative installs across them have passed 50 million. The longer you run a portfolio, the more these silent drifts in behavior — different prompt timing, different defaults, a typo in a key that nobody caught — quietly eat into the user experience. This is a quiet record of how I spent a few days bringing those five apps back into one consistent shape, with Antigravity Editor's multi-file editing keeping pace alongside me.

The variance was just early-stage copy-paste drift

The first thing I did was open the five repositories in parallel in Antigravity Editor and list every call site for SKStoreReviewController.requestReview. The conditions came back looking roughly like this.

  • App A: at least 5 launches, at least 90 days since the last prompt
  • App B: 3 launches, 60-day gap
  • App C: triggered after the user tapped "favorite" once, on the next cold launch
  • App D: 10 launches, no minimum gap at all
  • App E: 4 launches, and a "days since last prompt" check that, due to a typo in the UserDefaults key, had never actually been writing the date back

App E was the painful one. The prompt was firing, but the "when did we last show it" timestamp had never been recorded, which meant a user who declined could see the same dialog again disturbingly soon. The slightly higher rate of low ratings on that one finally made sense.

Maybe it is the influence of my two grandfathers, both temple carpenters, but small inconsistencies like this leave a knot in my chest. Treat the small joints carefully — that is one of the few unwritten things I inherited from them, and I think the same applies to tiny UX strings and dialogs.

Pulling the shared logic into one coordinator

The plan was to consolidate the scattered logic into a single ReviewPromptCoordinator.swift and let each app inject its own extra condition as a closure. I asked Antigravity Editor to create the same file in all five repositories, adjusting the import paths to fit each project. The diff preview opened as five tabs side by side, which made it easy to visually confirm each one before applying.

import StoreKit
import UIKit
 
public final class ReviewPromptCoordinator {
    public struct Config {
        let minLaunchCount: Int
        let minDaysSinceLastPrompt: Int
        let additionalCondition: () -> Bool
    }
 
    private let config: Config
    private let defaults: UserDefaults
    private static let lastPromptKey = "review.prompt.lastShownAt.v2"
    private static let launchCountKey = "review.prompt.launchCount.v2"
 
    public init(config: Config, defaults: UserDefaults = .standard) {
        self.config = config
        self.defaults = defaults
    }
 
    public func registerLaunch() {
        let current = defaults.integer(forKey: Self.launchCountKey)
        defaults.set(current + 1, forKey: Self.launchCountKey)
    }
 
    public func requestIfEligible(in scene: UIWindowScene) {
        guard isEligible() else { return }
        SKStoreReviewController.requestReview(in: scene)
        defaults.set(Date(), forKey: Self.lastPromptKey)
    }
 
    private func isEligible() -> Bool {
        let count = defaults.integer(forKey: Self.launchCountKey)
        guard count >= config.minLaunchCount else { return false }
        if let last = defaults.object(forKey: Self.lastPromptKey) as? Date {
            let days = Calendar.current.dateComponents([.day], from: last, to: Date()).day ?? 0
            guard days >= config.minDaysSinceLastPrompt else { return false }
        }
        return config.additionalCondition()
    }
}

I deliberately suffixed the UserDefaults keys with .v2. The point was to cleanly break the broken state in App E rather than migrate from a key that had never been written. Letting one extra prompt fire once for some users felt healthier, on balance, than dragging along a broken legacy state.

Per-app conditions stay as plain closures

With the shared base in place, each app just supplies the moment when a review prompt feels comfortable to ask for. For example, showing it only to users who have saved at least three favorite wallpapers.

let coordinator = ReviewPromptCoordinator(config: .init(
    minLaunchCount: 5,
    minDaysSinceLastPrompt: 120,
    additionalCondition: {
        FavoriteStore.shared.count >= 3
    }
))

Initializing this near the AppDelegate or App struct of each project made the flow far easier to read at a glance. I kept Antigravity Editor's diff views side by side, edited each of the five files in turn, and re-read the result myself before applying. I do not hand the final touches entirely over to the agent. That last bit of work I prefer to do with my own hands.

What the agent quietly caught for me

The genuinely helpful moment came as a note the agent left in the diff preview. App B's older code was double-incrementing the launch counter — once on actual launch, and once when the settings screen opened. I had not noticed it while I was busy stitching the new coordinator in. The new path was getting a correct launch count, but the old code that lived elsewhere was still inflating the legacy value.

The agent had calmly written, under the diff, "this call site would stay more consistent if it migrated to the new Coordinator as well." I do not enjoy talking about AI in superlatives, but having something quietly watch the edges where humans tend to cut corners is honestly useful.

How to communicate the App E bug

Because App E had been missing the "last shown" timestamp for some period of time, some users who declined the dialog likely saw it again sooner than they should have. I treated this as a question of honesty to those users and added one quiet line to the release notes.

It read, "due to an internal issue, the review prompt may have been shown more often than intended; this version corrects that." No drama, but also not hidden. That is roughly the temperature I want my work to have when I look at it later, including for my children who live apart from me.

Watching review prompt timing carefully

After the unification, I drew up a plan to split the minLaunchCount and the favorites-count threshold into two cohorts and quietly observe their indirect effect on AdMob rewarded views and retention through Firebase. That belongs in its own write-up, so I will not go deeper here. The point is that prompt timing affects not only the average rating but also the next-session return rate.

A few days of impressions

Before unification, the number of reviews per month between the top-performing app and the worst-performing one was differing by several multiples. It has only been a few days since the changes, so it is too early to claim anything decisive, but the inflow of low ratings does feel more settled. The temptation in a blog post is to declare an enormous improvement, but a few days of data is not enough to honestly say that. I let the implementation rest quietly and plan to look at the numbers in a few months.

What I will work on next

Using the same approach, I plan to unify the in-app notification copy and the empty-state text across the five apps. Since I started teaching myself programming in 1997 at the age of sixteen, I have been looking for the quiet satisfaction of staying in the same place and writing code with care. These small unification tasks sit very close to that feeling.

Thank you for reading. I hope this is useful to other indie developers running multiple apps in the same family.

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.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

App Dev2026-07-04
Paid, but the Ads Won't Go Away — Closing a StoreKit 2 Transaction.updates Launch Race with Antigravity
How I traced a StoreKit 2 launch race that dropped transactions arriving right after startup, pinned the listener to the app's lifetime instead of a view's, reconciled with currentEntitlements, and let Antigravity turn it into a StoreKitTest regression suite.
App Dev2026-06-23
When StoreKit 2 Users Say They Paid but Can't Access — Field Notes on Subscription Entitlement Drift
StoreKit 2 subscriptions are harder to operate than to implement. This is a record of the drift between currentEntitlements, subscription.status, and server notifications — and the reconcile logic that finally stopped the support tickets.
App Dev2026-04-01
Antigravity × SwiftData Practical Guide — Migrating from Core Data and Mastering the New Standard
Learn how to implement SwiftData — Apple's modern data persistence framework for iOS 17+ — with Antigravity's AI agents. Covers key differences from Core Data, basic CRUD operations, and a step-by-step migration guide with code examples.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →