โ† All articles

How to Add Subscriptions to a SwiftUI App with RevenueCat

Add subscriptions to a SwiftUI app with RevenueCat in 2026: dashboard setup, a paywall with RevenueCatUI, entitlement gating, restore, and testing.

How to Add Subscriptions to a SwiftUI App with RevenueCat

If you have decided to ship a paid SwiftUI app and picked RevenueCat to run the subscriptions, the good news is that the path from zero to a working paywall is short. RevenueCat sits on top of StoreKit 2, handles the receipt validation and the cross-device entitlement bookkeeping for you, and ships a SwiftUI paywall you can drop in with one modifier. The work that is left is mostly configuration, plus a handful of lines of Swift.

This guide walks the whole path: the mental model, the dashboard setup, the SDK, a paywall with RevenueCatUI, gating your content on an entitlement, the restore button Apple requires, and how to test before you ship. If you are still deciding between RevenueCat and rolling your own, read StoreKit 2 vs RevenueCat first; this post assumes you have landed on RevenueCat.

On this page

The mental model

Four concepts do the heavy lifting in RevenueCat, and the whole API makes sense once they click:

  • Product: a single thing a user can buy, defined in App Store Connect (for example a monthly and a yearly auto-renewable subscription). Prices and durations live here.
  • Entitlement: the level of access a purchase grants, named by you (for example pro). Your app checks entitlements, not products, so you can change or add products later without touching your gating code.
  • Offering: the set of products you present on a paywall. Offerings are configured in the dashboard, so you can swap pricing or run experiments without an app update.
  • Package: a product slotted into a standard duration inside an offering (monthly, annual, lifetime), which is what makes the same paywall code work across apps.

The flow is always the same: a user buys a product, the product is attached to an entitlement, and your app unlocks features when that entitlement is active. Keep your code pointed at the entitlement and the rest stays flexible.

Step 1: Configure products, entitlements, and an offering

This step happens in App Store Connect and the RevenueCat dashboard, before you write any Swift.

  1. In App Store Connect, create your auto-renewable subscription group and the products inside it (a monthly and a yearly is the common starting point). Set prices and a localized display name.
  2. In the RevenueCat dashboard, connect your app and add those products.
  3. Create an entitlement called pro and attach both products to it. From now on, owning either product grants pro.
  4. Create an offering (the default is named default) and add the products as packages (a Monthly package and an Annual package).

Practical rule: name the entitlement once and never rename it. Your Swift code, your paywall, and your analytics all reference that string, so a rename is a refactor across the whole stack.

Every new RevenueCat project also ships with a Test Store, so you can build and run the SDK before your App Store Connect products finish review. See RevenueCat's configuring products docs for the dashboard details.

Step 2: Add and configure the SDK

Add the SDK with Swift Package Manager. Point Xcode at https://github.com/RevenueCat/purchases-ios and add two libraries: RevenueCat (the core SDK) and RevenueCatUI (the prebuilt paywall).

Configure it once, as early as possible in the app lifecycle. The API key is the public SDK key from the dashboard, the one that starts with appl_.

import SwiftUI
import RevenueCat
 
@main
struct MyApp: App {
    init() {
        // Verbose logs while you build; drop to .info or .warn for release.
        Purchases.logLevel = .debug
        Purchases.configure(withAPIKey: "appl_yourPublicSDKKey")
    }
 
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

That is the entire setup. The SDK now knows how to fetch offerings, run purchases through StoreKit 2, and keep the customer's entitlement state in sync across devices and reinstalls.

Step 3: Show a paywall

RevenueCatUI gives you a real, configurable paywall without building one. The fastest version is a single modifier that presents the paywall only when the user lacks the entitlement, and dismisses it once they have it.

import SwiftUI
import RevenueCatUI
 
struct ContentView: View {
    var body: some View {
        HomeView()
            .presentPaywallIfNeeded(
                requiredEntitlementIdentifier: "pro",
                purchaseCompleted: { customerInfo in
                    // The user is now entitled. Unlock here if you need to.
                    _ = customerInfo.entitlements["pro"]?.isActive
                },
                restoreCompleted: { customerInfo in
                    // Restored purchases; paywall dismisses if "pro" is active.
                    _ = customerInfo.entitlements["pro"]?.isActive
                }
            )
    }
}

When you want the paywall behind a button instead (an "upgrade" screen, a locked feature), present PaywallView in a sheet. It fetches the current offering and renders the template you designed in the dashboard.

struct UpgradeButton: View {
    @State private var showPaywall = false
 
    var body: some View {
        Button("Go Pro") { showPaywall = true }
            .sheet(isPresented: $showPaywall) {
                PaywallView()
            }
    }
}

Because the paywall design lives in the dashboard, you can change copy, layout, and which products appear without shipping an update. That is the main reason teams reach for RevenueCatUI instead of hand-building a paywall in SwiftUI.

Step 4: Check entitlements and gate content

Anywhere in your app, ask the SDK whether the pro entitlement is active. The customerInfo object is the single source of truth, validated server-side.

import RevenueCat
 
func isProActive() async -> Bool {
    do {
        let info = try await Purchases.shared.customerInfo()
        return info.entitlements["pro"]?.isActive == true
    } catch {
        return false
    }
}

For UI that reacts to renewals, expirations, and purchases made on another device, observe the stream of updates instead of fetching once. A small @Observable model that mirrors the entitlement keeps your views in sync.

import RevenueCat
import Observation
 
@Observable
final class Entitlements {
    var isPro = false
 
    func start() async {
        for await info in Purchases.shared.customerInfoStream {
            isPro = info.entitlements["pro"]?.isActive == true
        }
    }
}

Drive the model from a .task on your root view, then branch your UI on entitlements.isPro. Because the stream pushes every change, a renewal that fails or a refund that lands flips the flag without you polling.

Step 5: Add a restore button

Apple requires a way to restore purchases for any app that sells non-consumable or subscription products. RevenueCatUI's paywall includes a restore control, but you should also expose one in Settings so a returning user is never stuck behind a paywall.

Button("Restore Purchases") {
    Task {
        let info = try? await Purchases.shared.restorePurchases()
        // info?.entitlements["pro"]?.isActive == true means they are back.
    }
}

Restore looks up the Apple ID's purchase history, re-validates it, and re-grants the matching entitlement. It is also the path that recovers a subscription after a reinstall or a new device.

Step 6: Test before you ship

You do not need live App Store products to test the full flow. Two options:

  • StoreKit configuration file: add a .storekit file in Xcode with your products, select it in the scheme, and the SDK runs purchases locally with no network or sandbox account. Fastest for day-to-day work.
  • Sandbox testers: create a sandbox Apple ID in App Store Connect and sign in on a device. This exercises the real StoreKit path, including renewals on an accelerated clock, which is the closest thing to production before release.

Verify three things end to end: the paywall shows the right packages and prices, a purchase flips your entitlement to active, and restore works from a clean install. Keep Purchases.logLevel = .debug on while you do this; the logs tell you exactly which offering loaded and which entitlement a purchase granted.

Setup checklist

StepWhereDone when
Products createdApp Store ConnectMonthly and yearly exist with prices
Entitlement proRevenueCat dashboardBoth products attached to it
Offering with packagesRevenueCat dashboarddefault offering has Monthly and Annual
SDK addedXcode (SPM)RevenueCat and RevenueCatUI import
Purchases.configureApp initRuns once with the appl_ key
PaywallSwiftUIpresentPaywallIfNeeded or PaywallView shows
GatingSwiftUIViews branch on the pro entitlement
RestoreSettingsA button calls restorePurchases()
TestedXcode or sandboxPurchase and restore both flip the flag

FAQ

Do I still need App Store Connect if I use RevenueCat?

Yes. Your products, prices, and subscription group live in App Store Connect, and Apple processes the payments. RevenueCat sits on top of that and handles validation, entitlements, analytics, and the paywall.

Why check entitlements instead of products?

So your gating code never changes when your catalog does. Add a lifetime option or a new price next year, attach it to the pro entitlement, and every isActive check keeps working untouched.

Is RevenueCatUI required, or can I build my own paywall?

It is optional. You can fetch the offering and build a fully custom SwiftUI paywall, then call Purchases.shared.purchase(package:) yourself. RevenueCatUI is the shortcut: a dashboard-configurable paywall with restore and loading states already handled.

How much does RevenueCat cost?

It is free up to $2,500 in monthly tracked revenue, then charges on the order of 1% of tracked revenue. Check their pricing page for current numbers. For the pricing side of subscriptions across markets, see App Store Subscription Pricing at WWDC26.

Do I need a server?

No. RevenueCat is the server piece. Entitlement state is validated and stored for you, which is the main reason indie teams use it instead of wiring StoreKit 2 and a backend by hand.

Will this work offline?

The last known entitlement state is cached on device, so a returning subscriber keeps access without a network round trip. New purchases and restores need a connection.

Adding subscriptions used to be the part of shipping a paid iOS app that ate a weekend. With RevenueCat the configuration is a dashboard exercise and the code is a configure call, a paywall modifier, and an entitlement check. Get those three right, test the purchase and restore flows, and the monetization layer is done.


Spaceport generates a production-ready SwiftUI Xcode project with exactly this wired up: RevenueCat configured, a paywall, entitlement gating, onboarding, sign-in, and App Store Connect subscription products and prices created across 25 markets via the App Store Connect API. You open the project, run it, and the subscription flow above already works, so you start from a shipping baseline instead of an empty Xcode project. From an indie iOS dev, for indie iOS devs.

Read more at spaceport.build

Community appsJoin Discord