How to Build a SwiftUI Paywall That Does Not Flicker Between Locked and Unlocked
Paywall flicker happens when entitlement checks, cached state, and UI rendering race each other. The user sees locked content for a split second even after purchase.
Step 1: Model entitlement as explicit state
enum AccessState {
case loading
case locked(reason: String)
case unlocked(expiry: Date?)
}
Step 2: Render with one top-level switch
switch store.access {
case .loading: LoadingView()
case .locked: PaywallView()
case .unlocked: LessonView()
}
Step 3: Update state from a single entitlement pipeline
func refreshEntitlement() async {
let result = await entitlementClient.fetch()
access = result.isActive ? .unlocked(expiry: result.expiresAt) : .locked(reason: "membership_required")
}
Pitfall
Calling entitlement APIs in multiple views. That creates conflicting state writes and visual jitter.
Verification
- No lock/unlock flicker during cold app launch.
- Purchase completion transitions to unlocked in one state update.
- Offline mode shows deterministic last-known state.