In-App Purchases Implementation in a SwiftUI App with StoreKit

The easiest and most straightforward way to monetize your iOS app is by offering in-app purchases (IAP) to your users. Apple has simplified the implementation of in-app purchases even further with the introduction of their updated StoreKit 2 framework. All you really need to focus on is what products to sell and how to deliver them.

In essence, the implementation of in-app purchases in a SwiftUI app consists of:

  1. Fetching products from the App Store
  2. Processing transactions
  3. Checking for existing purchases
  4. Listening for new transactions
  5. Displaying products in the UI

Let me show you an easy way to implement StoreKit consumable and non-consumable in-app purchases in a SwiftUI app.

Prerequisites

In this article I’ll be showing you an easy implementation of in-app purchases in SwiftUI using StoreKit, focusing on consumable and non-consumable products only, overlooking the subscription products. To follow along with this guide you’ll need to have:

Now, with this out of the way, let’s begin.

StoreKit Objects for In-App Purchases

The functionality we really care about when implementing in-app purchases with StoreKit is neatly abstracted away into two easy-to-use types:

  • Product – This type represents the products that we define on App Store Connect and has bunch of useful properties like ID, name, description, price, and so on;
  • Transaction – This type represents successful purchases. We use the properties of this type to check past purchases and listen for new ones.

Import StoreKit

Let’s begin by importing StoreKit module and creating an object that will handle all of our business logic. Conforming this object to @ObservableObject protocol helps us display in-app purchases in SwiftUI dynamically.

import StoreKit

class Store: ObservableObject {
    // We'll be writing our code here...
}

Fetch Products from App Store

To request products from App Store, you call the products(for:) method on  the Product type, providing a String collection of the product IDs that you defined in App Store Connect (or in the StoreKit Configuration file, if you’re testing in Xcode). 

One easy way to manage product IDs is to create a CaseIterable enumeration with String as the raw value, where each case represents an actual product ID.

enum Products: String, CaseIterable {
    case potionHealth, potionMagic, characterWizard, characterWitch
}

Using this enumeration you can generate product IDs collection by iterating over its cases like so:

let productIDs: Set<String> = {
    var result = Set<String>()
    for product in Products.allCases {
        result.insert(product.rawValue)
    }
    return result
}()

Now that you have product IDs ready, use them in the products(for:) call, and store the loaded products in the @Published property.

@Published var products: [Product] = []

// ...
    
@MainActor
func fetchProducts() async {
    do {
        let loadedProducts = try await Product.products(for: productIDs)
        products = loadedProducts
    } catch {
        // Handle errors here...
    }
}

It’s also a good idea to implement a retry mechanism to handle transient errors, but that’s out of scope of this article.

Verification and Processing of Transactions in StoreKit

In StoreKit, transaction verification and processing routines are repeated during checks for existing purchases and while listening for new ones. That’s why it’s a good idea to encapsulate these routines in two dedicated helper functions.

Transactions Verification in StoreKit

StoreKit automatically verifies transactions, so to access Transaction data of a successful purchases, you need to check whether a transaction was verified. The function below switches through VerificationResult cases and returns the relevant Transaction if it was verified, or throws an error otherwise.

func verify(_ result: VerificationResult<Transaction>) throws -> Transaction {
    switch result {
    case .unverified(let unverifiedTransaction, let verificationError):
        throw verificationError
    case .verified(let transaction):
        return transaction
    }
}

Processing Transactions in StoreKit

This is the creative part of the integration with StoreKit because you get to decide how you want deliver the purchased products to your users. 

This may range from storing simple values in UserDefaults to downloading additional content over the network. It’s really up to your business needs.

In the scope of this article, we’ll be storing non-consumable products in a @Published variable and consumables as a value in UserDefaults.

@Published var purchasedProducts: [Product] = []
    
// . . .
    
@MainActor
func process(_ transaction: Transaction) {
    if let purchasedProduct = products.first(where: { $0.id == transaction.productID }) {
        switch purchasedProduct.type {
        case .nonConsumable:
            purchasedProducts.append(purchasedProduct)
        case .consumable:
            let value = UserDefaults.standard.integer(forKey: purchasedProduct.id)
            UserDefaults.standard.set(value + 1, forKey: purchasedProduct.id)
        default:
            break
        }
    }
}

It’s a common practice to store consumable products in UserDefaults, as they are often understood to be used on per device basis. This means that if user buys a consumable product on one device, they won’t necessarily expect to have this product on their other device.

Checking for existing purchases in StoreKit

In order to deliver the products your users may already be entitled to, check for existing purchases by iterating through transactions in the currentEntitlements property of the Transaction type.

The following function makes use of the verify(_:) and process(_:) helper functions we defined above:

@MainActor
func refreshPurchasedProducts() async {
    for await result in Transaction.currentEntitlements {
        do {
            let transaction = try verify(result)
            process(transaction)
        } catch {
            // Handle errors here...
        }
    }
}

Listening for transactions updates in StoreKit

There are cases when the purchases happen outside of your app. For example, users might purchase your products on other devices they own, or there may be pending purchases due to “Ask to Buy” request (a parental control feature).

To check for new transactions in StoreKit you use the updates type property of Transaction to iterate through transactions in a background Task.

var updates: Task<Void, Never>? = nil

// . . .

func listenForUpdates() {
    updates = Task.detached {
        for await result in Transaction.updates {
            do {
                let transaction = try self.verify(result)
                await self.process(transaction)
                await transaction.finish()
            } catch {
                // Handle errors here...
            }
        }
    }
}

Initialization

Call the fetch, check, and updates functions we just defined during initialization, and cancel the transactions updates listener task during the de-initialization.

init() {
    listenForUpdates()
    Task {
        await fetchProducts()
        await refreshPurchasedProducts()
    }
}

deinit {
    updates?.cancel()
}

StoreKit integration with SwiftUI

Now onto the easy part — the UI implementation. To show the in-app purchases in SwiftUI, all you need to do is to import StoreKit, attach the business logic defined above as a @StateObject and use ProductView to display your products.

import SwiftUI
import StoreKit

struct ContentView: View {
    @StateObject var store = Store()
    
    var body: some View {
        List {
            Section("Potions") {
                ForEach(store.products.filter { $0.type == .consumable } ) { product in
                    ProductView(product) {
                        Text(product.id == Products.potionHealth.rawValue ? "❤️" : "💙")
                            .font(.system(size: 60))
                    }
                    .productViewStyle(.compact)
                }
            }
            Section("Characters") {
                ForEach(store.products.filter { $0.type == .nonConsumable } ) { product in
                    ProductView(product) {
                        Text(product.id == Products.characterWitch.rawValue ? "🧙‍♀️" : "🧙‍♂️")
                            .font(.system(size: 60))
                    }
                    .productViewStyle(.compact)
                }
            }
        }
    }
}

StoreKit’s ProductView displays product’s name, description, and localized price on a buy button. The coolest thing about ProductView though, is that it handles the purchase process for us, so we don’t need to write no additional  code. Users simply press the buy button on the ProductView and the StoreKit notifies us about successful purchases through updates listener.

Wrap-up

In conclusion, StoreKit integration with SwiftUI is a truly hassle-free process, leaving you more time to think about the stuff that really matters, like defining and designing the products you want to offer. I hope this walkthrough made this precess even simpler. Let me know what you think in the comments.

Discover more from Dudee

Subscribe now to keep reading and get access to the full archive.

Continue reading