StoreKit 2 subscriptions in SwiftUI: a practical guide

Everything you need to sell a subscription in a SwiftUI app with StoreKit 2, in one small Store class, plus the paywall rules App Review checks.

Mustafa GüderFounder, GuderTech4 min read
storekit-2-subscriptions-swiftui.md

StoreKit 2 turned in-app purchases from a delegate maze into a few lines of async Swift. This is the setup we use in our own apps: one small Store class, a paywall and a handful of rules that keep App Review happy.

1. Create the products

In App Store Connect, create a subscription group and add your plans, for example monthly and yearly. Note the product IDs. For local testing, add a StoreKit Configuration File in Xcode with the same IDs and select it in your scheme. You can then buy, renew and cancel in the simulator without a sandbox account.

2. One Store for the whole app

import StoreKit

@MainActor
final class Store: ObservableObject {
    @Published private(set) var products: [Product] = []
    @Published private(set) var isPro = false

    private let ids = ["com.example.pro.monthly", "com.example.pro.yearly"]
    private var updates: Task<Void, Never>?

    init() {
        // Renewals, refunds and purchases made on other devices arrive here.
        updates = Task { [weak self] in
            for await result in Transaction.updates {
                await self?.handle(result)
            }
        }
        Task {
            await loadProducts()
            await refreshEntitlements()
        }
    }

    func loadProducts() async {
        do {
            products = try await Product.products(for: ids).sorted { $0.price < $1.price }
        } catch {
            print("Could not load products: \(error)")
        }
    }

    func buy(_ product: Product) async throws {
        switch try await product.purchase() {
        case .success(let result):
            await handle(result)
        case .userCancelled, .pending:
            break
        @unknown default:
            break
        }
    }

    func restore() async {
        try? await AppStore.sync()
        await refreshEntitlements()
    }

    func refreshEntitlements() async {
        var active = false
        for await result in Transaction.currentEntitlements {
            if case .verified(let transaction) = result, transaction.revocationDate == nil {
                active = true
            }
        }
        isPro = active
    }

    private func handle(_ result: VerificationResult<Transaction>) async {
        guard case .verified(let transaction) = result else { return }
        await transaction.finish()
        await refreshEntitlements()
    }
}

Create it once, for example as a @StateObject in your App, and pass it down with .environmentObject. Start listening to Transaction.updates as early as possible; otherwise purchases completed outside the app can be missed.

3. Why verification matters

Every transaction comes wrapped in a VerificationResult. StoreKit checks Apple's signature on the device, so .verified means the purchase is genuine. Only unlock content for verified transactions, and always call finish() once you have delivered it.

4. A paywall in a few lines

On iOS 17 and later, SubscriptionStoreView gives you a complete, App Review friendly paywall:

SubscriptionStoreView(groupID: "YOUR_GROUP_ID") {
    VStack(spacing: 8) {
        Text("Go Pro").font(.largeTitle.bold())
        Text("Unlimited exports and every template.")
    }
}
.storeButton(.visible, for: .restorePurchases)

If you design your own, list each plan's price and period with product.displayPrice, show the terms and privacy links, and include a restore button.

5. Free trials and intro offers

Configure introductory offers in App Store Connect. To show "Start free trial" only to people who can use it, check eligibility first:

let eligible = await product.subscription?.isEligibleForIntroOffer ?? false

6. What App Review checks on a paywall

  • Price and billing period are clear before purchase
  • Free trial length and what happens after it
  • A working restore purchases button
  • Links to your terms of use and privacy policy
  • No dark patterns, such as a hidden close button

Our list of the most common App Store rejections covers the rest.

7. Server side, when you need it

For a single-device app, on-device entitlements are enough. If users sign in on several platforms, or you need revenue data, add App Store Server Notifications V2 to your backend so your server learns about renewals and refunds as they happen.

Frequently asked questions

Do I still need receipt validation with StoreKit 2?

Not on the device: StoreKit 2 verifies signed transactions for you. A server is only needed if you share entitlements across platforms or accounts.

How do I test subscriptions without paying?

Use a StoreKit Configuration File in Xcode for local tests, then a sandbox account or TestFlight for end-to-end tests. Sandbox renewals run on an accelerated schedule.

What is the minimum iOS version for StoreKit 2?

StoreKit 2 works on iOS 15 and later. SubscriptionStoreView and the other merchandising views need iOS 17.

How much does Apple take from subscriptions?

30% by default, 15% for members of the Small Business Program, and 15% on a subscription after the subscriber's first year.

ShareXLinkedInWhatsApp
Mustafa Güder

About the author

Mustafa Güder

Mustafa Güder is the founder of GuderTech and an iOS developer. After two years of study in Australia he came home to Adana and built Scevia, Paw & Home and Street Legends.

Studied in Australia, 2 years Adana, Türkiye

All posts