Home
Blog
How to add in-app purchases in SwiftUI with Store Kit 2: A Comprehensive Guide

How to add in-app purchases in SwiftUI with Store Kit 2: A Comprehensive Guide

Portrait photo of blog author
Kendall Gelner
Senior iOS Engineer

Incorporating in-app purchases (IAPs) is a crucial step for monetizing your iOS app. With the introduction of StoreKit 2, Apple has simplified the process, offering powerful new tools and a more straightforward API. This guide will walk you through the steps to add in-app purchases to your SwiftUI app using StoreKit 2, ensuring a seamless experience for both developers and users.

Table of Contents:

In this blog post:

What's SwiftUI?

SwiftUI is a modern framework introduced by Apple for building user interfaces across all Apple platforms using a declarative Swift syntax. Launched in 2019, SwiftUI allows developers to create robust, dynamic, and interactive user interfaces with less code and more efficiency. Instead of manually managing the state of the UI, SwiftUI handles it automatically, allowing developers to focus on defining what the UI should look like for any given state.

SwiftUI works seamlessly with other Apple frameworks and is designed to be integrated with existing UIKit and AppKit projects.

How to add in-app purchases in SwiftUI with Store Kit 2: Step-by-Step

Step 1: Configure Your Project in App Store Connect

1.1 Create an App Record

  1. Log in to App Store Connect.
  2. Go to "My Apps" and click the "+" button to create a new app.
  3. Enter the required information and save.

1.2 Set Up In-App Purchases

  1. Select your app and go to the "Features" tab.
  2. Click on "In-App Purchases" and then the "+" button to add a new in-app purchase.
  3. Choose the type of in-app purchase (consumable, non-consumable, or subscription).
  4. Fill in the necessary details like Reference Name, Product ID, and Pricing.
  5. Submit the in-app purchase for review.

Step 2: Implementing In-App Purchases with StoreKit 2

2.1 Import StoreKit

First, import the StoreKit framework to access the necessary classes and methods for implementing in-app purchases:

import StoreKit


StoreKit is the framework provided by Apple to handle in-app purchases and subscriptions. By importing StoreKit, you gain access to various tools and functions that simplify the process of fetching products, managing transactions, and verifying purchases. StoreKit 2, introduced in iOS 15, enhances this functionality by offering a more modern, Swift-friendly API that leverages Swift concurrency.

2.2 Define Your Products

Create a class to manage in-app purchase products using StoreKit 2:

class IAPManager: ObservableObject {
   @Published var products: [Product] = []
   
   init() {
       Task {
           await self.retrieveProducts()
       }
   }
   
   func retrieveProducts() async {
       do {
           let productIDs = ["com.yourapp.productid1", "com.yourapp.productid2"]
           products = try await Product.products(for: productIDs)
       } catch {
           print("Failed to fetch products: \(error)")
       }
   }
}


This IAPManager class handles the fetching of available products from the App Store. The @Published property wrapper allows SwiftUI views to reactively update whenever the products array changes.

2.3 Purchase Handling

Add methods for purchasing and handling transactions:

extension IAPManager {    func purchase(_ product: Product) async -> Bool {
       do {
           let result = try await product.purchase()
           switch result {
           case .success(let verification):
               let transaction = try self.verifyPurchase(verification)
               await transaction.finish()
               return true
           case .userCancelled, .pending:
               return false
           @unknown default:
               return false
           }
       } catch {
           print("Purchase failed: \(error)")
           return false
       }
   }

   private func verifyPurchase(_ verification: VerificationResult<Transaction>) throws -> Transaction {
       switch verification {
       case .unverified:
           throw NSError(domain: "Verification failed", code: 1, userInfo: nil)
       case .verified(let transaction):
           return transaction
       }
   }
}

The purchase method handles the entire purchasing process, including starting the purchase, verifying the transaction, and finishing it. The verifyPurchase method ensures the transaction is valid and comes from the App Store.

2.4 Integrate with SwiftUI

Use the IAPManager in your SwiftUI views:

struct ContentView: View {
   @StateObject var iapManager = IAPManager()
   
   var body: some View {
       List(iapManager.products, id: \.id) { product in
           VStack(alignment: .leading) {
               Text(product.displayName)
               Text(product.description)
               Text(product.displayPrice)
               Button("Buy") {
                   Task {
                       await iapManager.purchase(product)
                   }
               }
           }
       }
   }
}


This SwiftUI view displays a list of available products, with buttons to initiate the purchase process. The @StateObject property wrapper ensures that the IAPManager instance is created and retained correctly within the view.

Step 3: Testing In-App Purchases

3.1 Create a StoreKit Configuration File

  1. Create a StoreKit configuration file in Xcode.
  2. Add your in-app purchase products to this file.

3.2 Enable StoreKit Configuration in Scheme

  1. Edit your scheme in Xcode.
  2. Under "Run" > "Options", select your StoreKit configuration file.

Step 4: Advanced StoreKit 2 Features

4.1 Handling Subscriptions

To manage subscriptions, you need to handle the different states a subscription can be in, such as active, expired, or in a billing retry period.

func handleSubscriptions() async {
   do {
       let statuses = try await Product.subscriptions.allStatuses()
       for status in statuses {
           switch status.state {
           case .subscribed:
               // User is subscribed
               break
           case .expired:
               // Subscription expired
               break
           case .inBillingRetryPeriod:
               // Billing issue
               break
           default:
               break
           }
       }
   } catch {
       print("Failed to fetch subscription statuses: \(error)")
   }
}


4.2 Restoring Purchases

Users expect to be able to restore purchases, especially if they switch devices. Implement a restore function to handle this.

func restorePurchases() async {
   do {
       try await AppStore.sync()
       try await updateUserPurchases()
   } catch {
       print("Failed to restore purchases: \(error)")
   }
}


4.3 Transaction Listener

Listening for transaction updates ensures you catch any purchases made on other devices or pending purchases.

@MainActor
func listenForTransactions() {
   Task {
       for await verificationResult in Transaction.updates {
           do {
               let transaction = try self.verifyPurchase(verificationResult)
               // Handle the transaction
               await transaction.finish()
           } catch {
               print("Transaction verification failed: \(error)")
           }
       }
   }
}


4.4 Updating User Purchases

Keep track of the user's purchased items to unlock content appropriately.

@MainActorfunc updateUserPurchases() async {
   do {
       for await verificationResult in Transaction.currentEntitlements {
           let transaction = try self.verifyPurchase(verificationResult)
           switch transaction.productType {
           case .consumable:
               // Handle consumable purchase
               break
           case .nonConsumable:
               // Handle non-consumable purchase
               break
           case .autoRenewable:
               // Handle subscription
               break
           default:
               break
           }
       }
   } catch {
       print("Failed to update user purchases: \(error)")
   }
}

Different kind of purchases available

There are different kinds of data you can hold purchase activity in, how to write code to handle purchase activity with StoreKit, or use a framework like Nami which manages StoreKit transactions for you and can work with data stores like the ones described below.

Subscriptions

As subscriptions are a model where a customer gives you money over time in return for some continuing value of your application, it's a good idea to keep in mind the customer journey with your application - are they brand new?  Might they have been using your app for some time and are about to subscribe?  If they have subscribed, are they going to renew, or have they opted to cancel a current subscription and let it just run out?

Fundamentally, the simplest things you would want to track for a subscription are:

Just from those, you can adjust displays in your application to access content, or to provide some additional messaging as the end of a subscription draws near.

One-Time Purchases

This is what most people think of when they talk about In-App Purchases. In this case, the purchase is made only once and lasts forever.  So it's enough to simply track if something is purchased.

Consumable Purchases

Consumable purchases allow a user to make the same purchase more than once.  Two common use cases are coin packs in games and apps that let you buy and spend credits.  For another uses case of consumable purchases, see our guide on creating a tip jar for your app.

Since users can make a purchase more than once, it's a good idea to keep track of how many times they may have purchased, for either messaging around thanking them for each purchase or adapting the application to reward multiple purchases in some way as well as the user’s credit balance:

You can add more details around any of those items, but those are great starting places.

Where to Put Purchase Data?

When you decide what kind of purchase data you want to preserve and react to, you then need to decide how to store it in a way that a SwiftUI application can react to it.  In order to do that, you can make use of the Combine framework, which allows you to have an object that publishes changes.  For SwiftUI, that means making an ObservableObject, with Published properties that correspond to the kinds of things you'll be looking for.

Creating your ObservableObject

An Observable object is made by declaring one or more Published properties, that when changed will notify any views using the properties.  

For this example, we’ll create an ObservableObject for a subscription.

Then you can either add methods to the ObservableObject to alter the Published properties as needed, or modify them externally via some other code.  An example of setting up a listener for an Observable object to change purchase state for the object above would be to add an init like so:

When you process a purchase with StoreKit, check to see that the purchase has completed, and then send a notification that triggers the ObservableObject to update values:

Note that properly validating a purchase requires looking at the receipt which should be done on a server.  Take a look at this blog post to get started.

Correctly updating subscription state over time also requires a server to process Apple’s Server to Server notifications and correctly update a model of the customer’s subscription lifecycle.  This is beyond the scope of this article.  

If you are using the Nami ML platform, we automatically manage the subscription lifecycle for you.  The code sample below provides an example of fully managing a customer’s access to a subscription in a SwiftUI app.

Creating and Accessing ObservableObject

Once you have an ObservableObject, you'll need to be able to use it for a variety of views across your application.  You also want to create that ObservableObject as early as possible, since purchase messages might be triggered as soon as your application launches.

You could simply create an instance of the object and pass it into every view via an initializer, but chances are you will not need to know about purchase status in every view.  Thus, it's easier to add your ObservableObject into the application environment where any view can access it directly without having to have been passed in the object.

The best place to do setup and add the ObservableObject to the environment is in your App class, where your initial Scene is created.

Now in any class that you want to use your ObservableObject properties, you can just add the object from the environment.

Allowing Access to Paid Content and Features

When your users make a purchase, that purchase unlocks an entitlement that grants them access to the paid features of your app.

Now that we have all the basics in place, let’s look at a few different options of how you can grant access to paid features in your app.

View Visibility

You can optionally display a view, based on the current state of purchases.

You can make an even more complex choice, deciding to display a view based not just on purchase state, but on some other variable like subscription expiration.

In this example, there may be a completely different view for a paid subscriber than there is for a free user.

Conditional View Content

In the same way, you can conditionally add whole views, you can also opt to change content based on the purchase state.

This can be a good way to show paid features that exist on a view or enable a disabled button that does not work for a free customer.

Navigation

When you have a button that accesses paid content in your application, you can check if the user has the correct access for that content.

If they do, allow them to see the content, otherwise you can present your paywall with your purchase options.

This is a general pattern you may use to protect paid content in your app.

Integrating Full Purchase Support

A full StoreKit implementation is beyond the scope of this article and requires some updates to your app code as well as some server-side components as well.  This talk has a good overview of the basics.

The Nami platform also takes care of the complexity of StoreKit integration so you can focus on other aspects of your app.  Check out our SwiftUI page or Quickstart Guide for an overview.

SwiftUI makes it very easy to modify UI in reaction to purchases via waiting for state changes.  You should think about adding support for purchases as early as possible during the design of your application, so the integration of purchase boundaries feels as natural as possible and doesn't cause you extra work rearranging UI to support purchases later.

Kendall Gelner is a Senior iOS Engineer at Fieldwire. Previous, he was the founding iOS Architect at Nami ML. He is well regarded in the iOS development community for his technical knowledge and platform experience going back to the App Store launch. The last SDK Kendall was responsible for shipped inside of some of the most widely installed apps, reaching more than 200 million devices.

Nami® logo

Maximize your App's Potential

Accelerate app revenue with Nami subscriptions.

Nami® logo

Focus on your app experience

We'll handle the subscriptions.

Sign up to our newsletter

Get the latest articles delivered straight to your inbox.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Similar articles

Read similar articles to this one