Back to Journal
LaunchFreely

App Store Rejection: The Infinite Splash Screen

How a race condition in async sign-in code created an infinite splash screen that killed V1.0's App Store submission.

February 25, 20265 min read

V1.0 of Freely was submitted to the App Store on February 20th. I was cautiously optimistic. The app worked. Tests passed. I'd tested on real devices. What could go wrong?

A race condition. A race condition could go wrong.

The Rejection

Apple's review team hit Guideline 2.1: Performance. The app got stuck on an infinite splash screen after sign-in. Just a logo, forever, mocking the reviewer who was trying to evaluate my carefully crafted allergen detection features.

The Root Cause

Freely's launch flow checks three things in sequence: authentication state, subscription status, and onboarding completion. The splash screen stays visible until all three resolve.

The problem: when a user signs in during the initial .task block, the subscriptionChecked flag wasn't being set. The auth state updated, the onboarding state updated, but the subscription check never completed. The splash screen was waiting for a signal that would never come.

In my testing, this never happened because I was already signed in. The race condition only triggered on a fresh install where the user signs in for the first time during the splash screen's initialization. Which is exactly what an App Store reviewer does.

The Fix

Two changes:

Safety timeout. If the splash screen hasn't resolved after 10 seconds, force-dismiss it and proceed to the appropriate screen. No user should stare at a splash screen for more than a few seconds regardless of what async work is happening underneath.

Subscription retry. The PaywallView now retries the product fetch if the product list is empty when the user taps "Subscribe." This handles the case where StoreKit products haven't loaded yet, which was the second half of the race condition.

// The 10-second safety net
.task {
    try? await Task.sleep(for: .seconds(10))
    if !hasResolved {
        hasResolved = true
        // Force proceed - something is stuck
    }
}

What I Learned

Test the fresh install flow. Every time. I had tested on devices where I was already signed in. The happy path worked perfectly. The first-time user path, the one that every App Store reviewer will take, was broken.

Add timeouts to sequential async gates. If your app's launch depends on multiple async checks completing, any one of them can silently fail and leave the user stranded. Timeouts are ugly but they're better than infinity.

Reviewers are users too. They don't know your app, they don't have saved credentials, and they don't have patience for loading screens. If the first 10 seconds of your app don't work flawlessly for a complete stranger, you'll get rejected. Rightfully so.

V1.2 shipped with both fixes plus a redesigned onboarding funnel (cut from 12 screens to 4 pre-paywall). The rejection was frustrating in the moment but forced improvements I should have made before submitting.

Sometimes Apple's review process is the beta tester you forgot to invite.