How to use In-App Purchases in SwiftUI apps

Share on facebook
Share on twitter
Share on pinterest

Welcome to a new SwiftUI tutorial! Today we will learn how to integrate and use In-App-Purchases in SwiftUI apps to monetize our app. For this, we will write a small app called “Treasure Store” where the user can buy different items like weapons or skins. Don’t worry. We will go through everything step by step and explain it as simple as possible. After this tutorial, you will be able to monetize your SwiftUI app with In-App-Purchases.

Important: To use and test In-App-Purchases in SwiftUI, you need both a physical Apple device and a paid Apple Developer account!

At the end of the tutorial, our app will look like this:

Creating an Xcode project and preparing our ContentView 👩‍🎨

Let’s start with the preparation of our Xcode project. Open Xcode 12 and create a new Xcode project. Select “App” under “iOS” or “Multiplatform” and click on “Next”. As the “Product Name” we choose “Treasure Store”. At this point, you can also enter an Organization Identifier, e.g., your name, which will be used to generate the Bundle Identifier.

Click on “Next” and then create the Xcode project.

We will only be using one main view for our app. For this, we can use the by default generated ContentView.swift file.

The ContentView should contain a List that will later show the available IAP products. Until we have set this up, we will use dummy data.

struct ContentView: View {
    var body: some View {
        List {
            
        }
    }
}

We want to display the product’s name, description, and price for each IAP product that we will later obtain from Apple servers. We also use a Button to start the purchase process later.

List {
    HStack {
        VStack(alignment: .leading) {
            Text("Power Sword")
                .font(.headline)
            Text("Dominate your enemies with this noble weapon")
                .font(.caption2)
        }
        Spacer()
        Button(action: {
            //Purchase particular IAP product
        }) {
            Text("Buy for 1.09 $")
        }
            .foregroundColor(.blue)
    }
}

Your ContentView preview should now look like this:

We determine whether the user has purchased the corresponding product by checking whether the respective IAP product ID (which will also be provided to us by the Apple servers) has a UserDefault value. If this is the case, we want to show the user a “Purchased” Text instead of the “Purchase” Button. So we write:

HStack {
    //...
    Spacer()
    if UserDefaults.standard.bool(forKey: "*ID of IAP Product*") {
        Text("Purchased")
            .foregroundColor(.green)
    } else {
        Button(action: {
            //Purchase particular IAO product
        }) {
            Text("Buy for 1.09 $")
        }
            .foregroundColor(.blue)
    }
}

Next we wrap our List into a NavigationView and add a .navigationTitle to it.

NavigationView {
    List {
        //...
    }
    .navigationTitle("Treasure Store 🏴‍☠️")
    .toolbar(content: {
        ToolbarItem(placement: .navigationBarTrailing) {
            Button(action: {
                //Restore products already purchased
            }) {
                Text("Restore Purchases ")
            }
        }
    })
}

Finally, we want to provide the user with a Button in the navigation bar to restore purchases already made. For this, we use the .toolbarModifier and initialize a ToolbarItem, which we place on the right side.

Great! We are already done with the preparation of the ContentView. The corresponding preview should now look like this:

Creating an App ID 👨‍💻

First, we have to create an App ID in the Apple Developer Center so that Apple’s server can correctly assign our app. This is quite simple. Visit the Apple Developer Center, log into your account if necessary, and go to “Certificates, Identifiers & Profiles”. 

Now open the “Identifiers” tab and create a new ID by clicking on the plus icon. Then select “App IDs”, click on Continue and select “App” again. 

Now it gets crucial. In the box “Description”, you can enter the name of your app. Next, we enter a unique Bundle ID. If your Xcode project had already generated one for you when you created it, it is a good idea to use it. Otherwise, you can also think of your own. Important: The Bundle Identifier of your Xcode project must match the Bundle ID in the Developer Center!

Quickly make sure that “In-App Purchase” is selected under “Capabilities” and click on “Register”. 

Checking pending agreements ✅

Next, we will make the necessary preparations for our In-App Purchases in App Store Connect. In App Store Connect, we can provide and manage content for the App Store.

It is very important that you have completed the necessary forms and accepted the agreements. Otherwise, you will not be able to communicate with the Apple servers.

Visit this link or open App Store Connect and select “Agreements, Tax and Banking”.

If both the agreements for Paid Apps and Free Apps have the status “Active”, you can continue.

Creating an App, Sandbox users, and In-App Purchases in App Store Connect 🌎

Now we can register the Treasure Store App in App Store Connect. Visit this link or open App Store Connect and select “Apps”.

To register a new app, click on the plus icon and choose “New App”.

Now we have to provide the necessary information for the app entry. Select the platform of your app (in our case iOS). Enter “Treasure Store” as the name and select the primary language.

As the Bundle ID, we now choose the one we created in the Developer Center! As the SKU, you can choose any identifier you like. When you are done, click on “Create”.

Next, we create different In-App Purchases products for our SwiftUI app. Open the tab “Manage” under “In-App-Purchases” and click on the plus icon to add a new IAP product.

You can now choose between four different types of In-App Purchases.

In this tutorial, we will only deal with Non-Consumable In-App Purchases, i.e., products purchased only once by the user. With the knowledge you acquire here, you will quickly understand how to deal with the other types of In-App Purchases too.

After clicking “Create”, you will be redirected to a new site. Choose a proper reference name here and enter a unique Product ID. Here you can use a combination of the Bundle ID of your app and the name of your IAP. Also, choose a pricing tier for your product. By the way: as long as you don’t publish your app, you don’t have to provide meta information.

We also provide a title and a description for the IAP as it will be displayed by the App Store.

Repeat these steps for other IAP products you want to offer in the Treasure Store app!

Before we get to the code, we need to create some sandbox text users. Of course, we don’t want to test the IAP with our personal Apple ID and pay real money at the end.

Open App Store Connect and click on “User and Access”. Open the tab “Testers” under “Sandbox”.

Here we create a fictive test user. But make sure you remember the password because you can’t change it later. We also have to enter a real mail address and confirm it. Note that this mail address must not already be connected for a real Apple ID!

Once you’re finished, click on “Invite”.

You can also create a few more sandbox users to test the purchase of a product several times later.

Great, we have now both registered our app in App Store Connect, created various IAP products, and created sandbox user accounts for testing purposes.

Next, we will switch back to our Xcode project.

Activating In-App Purchases in our Xcode project

Back in our Xcode project, we only need to open the “Signing & Capabilities” tab in our Dungeon Store target settings.
We now open the “Capabilities” Library by clicking on the “Plus” button in the Xcode toolbar.

We now search for “In-App-Purchase” and double click on the entry to add the corresponding capability to our app.

We have now made all necessary preparations and can finally start writing the code for embedding the In-App Purchases!

Setting up the StoreManager

We need a place in our SwiftUI app where we can fetch the In-App Purchases products from the Apple servers and start the purchase process for a specific product at the user’s request.

To do this, we create a File-New-File and select “Swift File”. We call this file “StoreManager”. Import the StoreKit framework and create a class with the same name.

import Foundation
import StoreKit

class StoreManager {
    
}

Since we want to notify our ContentView later, as soon as the products are fetched from App Store Connect, we conform the StoreManager to the ObservableObject protocol. If you don’t know what this is all about, please have a look at this tutorial.

class StoreManager: ObservableObject {
    
}

We want to initialize the StoreManager as soon as we launch our Treasure Store app. Therefore we switch to the Treasure_StoreApp struct and initialize the StoreManager as a @StateObject.

@main
struct Treasure_StoreApp: App {
    
    @StateObject var storeManager = StoreManager()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Our ContentView should have access to exactly this storeManager and observe it. Therefore, we also declare a storeManager property in our ContentView

struct ContentView: View {
    
    @StateObject var storeManager: StoreManager
    
    var body: some View {
        //...
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(storeManager: StoreManager())
    }
}

… and pass the storeManager of our Treasure_StoreApp struct to the ContentView:

@main
struct Treasure_StoreApp: App {
    
    @StateObject var storeManager = StoreManager()
    
    var body: some Scene {
        WindowGroup {
            ContentView(storeManager: storeManager)
        }
    }
}

Fetching IAP Products using the SKProductsRequestDelegate protocol 

We want to use our StoreManager to fetch the IAP products we have created in App Store Connect. Once we have received them, we use the information contained in them, such as the product name, description and price, to display the available IAPs to the user in our ContentView‘s List.

To fetch the products, we adapt the NSObject and SKProductsRequestDelegate protocols.

class StoreManager: NSObject, ObservableObject, SKProductsRequestDelegate {
    
}

To conform to the SKProducutsRequestDelegate, we add the following function to our class:

func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
    
}

As soon as we receive a response from App Store Connect, this function is called. 

We now declare an array that will contain the IAP products as SKProduct instances. Since we want to update the observing ContentView every time a new SKProduct is added, we use the @Published property wrapper.

@Published var myProducts = [SKProduct]()

We also need a SKProductsRequest property in our StoreManager, which we will use to start the fetching process. 

var request: SKProductsRequest!

Next we implement a function that sends a request to the Apple servers based on given product IDs. At the same time we use the StoreManager class itself as the delegate of the request, so that the request knows that the didReceive response method should be called as soon as the Apple servers send a response.

func getProducts(productIDs: [String]) {
    print("Start requesting products ...")
    let request = SKProductsRequest(productIdentifiers: Set(productIDs))
    request.delegate = self
    request.start()
}

As said, the didReceive response method is called as soon as we get a response from the Apple servers. As soon as this is the case, we check if the response also contains products.

func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
    print("Did receive response")
    
    if !response.products.isEmpty {
        
    }
}

If we are sure that we have received products, we can add any of these products to our myProducts array using a for-in loop.

if !response.products.isEmpty {
    for fetchedProduct in response.products {
        DispatchQueue.main.async {
            self.myProducts.append(fetchedProduct)
        }
    }
}

If for some reason our response contains product IDs that are invalid, we want to be notified. We use a corresponding for-in loop for this purpose as well.

for invalidIdentifier in response.invalidProductIdentifiers {
    print("Invalid identifiers found: \(invalidIdentifier)")
}

The overall didReceive response method now looks like this:

func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
    print("Did receive response")
    
    if !response.products.isEmpty {
        for fetchedProduct in response.products {
            DispatchQueue.main.async {
                self.myProducts.append(fetchedProduct)
            }
        }
    }
    
    for invalidIdentifier in response.invalidProductIdentifiers {
        print("Invalid identifiers found: \(invalidIdentifier)")
    }
}

If it should happen that we get no response from the Apple servers but the request fails, we would like to be notified about this.

Therefore, we add the following method to our StoreManager class:

func request(_ request: SKRequest, didFailWithError error: Error) {
    print("Request did fail: \(error)")
}

Updating the App struct and the ContentView 🔄

Once our ContentView is initialized as our app’s root view, we want to start the fetching process. For this purpose, we use the .onAppear modifier for the ContentView instance in our Treasure_StoreApp struct. 

WindowGroup {
    ContentView(storeManager: storeManager)
        .onAppear(perform: {
            storeManager.getProducts(productIDs: )
        })
}

Important: As the productIDs, you must now use the IDs you used for the In-App Purchases you created in App Store Connect earlier!

You can put them into your own array, which you use for the productIDs parameter of the getProducts function.

@main
struct Treasure_StoreApp: App {
    
    let productIDs = [
        //Use your product IDs instead
        "com.BLCKBIRDS.TreasureStore.IAP.PowerSword"
        "com.BLCKBIRDS.TreasureStore.IAP.HealingPotion"
        "com.BLCKBIRDS.TreasureStore.IAP.PirateSkin"
    ]
    
    @StateObject var storeManager = StoreManager ()
    
    var body: some Scene {
        WindowGroup {
            ContentView(storeManager: storeManager)
                .onAppear(perform: {
                    storeManager.getProducts(productIDs: productIDs)
                })
        }
    }
}

The next step is to update our ContentView. The List should use the myProducts array of the storeManager to present one row for each SKProduct instance contained.

List(storeManager.myProducts, id: \.self) { product in
    //...
}

Now we replace the fixed values of the two Text views and the UserDefault key with the values of the respective product.

List(storeManager.myProducts, id: \.self) { product in
    HStack {
        VStack(alignment: .leading) {
            Text(product.localizedTitle)
                .font(.headline)
            Text(product.localizedDescription)
                .font(.caption2)
        }
        Spacer()
        if UserDefaults.standard.bool(forKey: product.productIdentifier) {
            Text ("Purchased")
                .foregroundColor(.green)
        } else {
            Button(action: {
                //Purchase particular ILO product
            }) {
                Text("Buy for \(product.price) $")
            }
                .foregroundColor(.blue)
        }
    }
}

Now let’s run the SwiftUI app on a physical device and see if everything works and the In-App Purchases are being loaded.

Perfect! As soon as the ContentView is launched as the root view, the getProducts function of the storeManager starts to fetch the IAP products of App Store Connect using the given IDs. 

We then use the obtained SKProduct instances to represent them in our List.

Purchase products using the SKPaymentTransactionObserver protocol

Okay, we are now able to fetch the IAP products from App Store Connect. We need to start the purchase process as soon as the user taps on the respective “Purchase” Button.

For this functionality, our StoreManager class has to adopt the SKPaymentTransactionObserver protocol. To conform to this protocol, we have to add the “paymentQueue” method to our StoreManager class.

class StoreManager: NSObject, ObservableObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
    
    //...
    
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        
    }
}

Once we have started a transaction (which we will do in a moment), this function is called every time something changes in the status of the transaction(s) currently processed.

To start a transaction, we implement a new function called “purchaseProducts” which accepts a SKProduct.

func purchaseProduct(product: SKProduct) {
        
        
}

First of all, we make sure that the user can also make payments. This is not the case, for example, if parental control is set up.

func purchaseProduct(product: SKProduct) {
    if SKPaymentQueue.canMakePayments() {
        
    } else {
        print("User can't make payment.")
    }
}

If the user can make payments, we initiate a payment process using the given product and add it to the SKPaymentQueue. The SKPaymentQueue handles all payments to be processed on communicates to the App Store Connect servers.

func purchaseProduct(product: SKProduct) {
    if SKPaymentQueue.canMakePayments() {
        let payment = SKPayment(product: product)
        SKPaymentQueue.default().add(payment)
    } else {
        print("User can't make payment.")
    }
}

At the same time, we also have to make sure that something is observing the queue and responding to changes. This should also be our StoreManager. So we go to our Treasure_StoreApp struct and set the storeManager as the observer for the SKPaymentQueue by writing:

ContentView(storeManager: storeManager)
    .onAppear(perform: {
        SKPaymentQueue.default().add(storeManager)
        //...
    })

Next, we add a SKPaymentTransactionState @Published property to our StoreManager class so that we can notify our ContentView each time the status of the processed transaction changes.

@Published var transactionState: SKPaymentTransactionState?

As mentioned above, the “paymentQueue” is called every time the status of the processed transaction(s) changes. If this happens, we want to update the @Published transactionState property for each processed transaction, depending on the transactionState of this transaction, and thus notify the ContentView. Therefore we write:

func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    for transaction in transactions {
        switch transaction.transactionState {
        case .purchasing:
            transactionState = .purchasing
        case .purchased:
            transactionState = .purchased
        case .restored:
            transactionState = .restored
        case .failed, .deferred:
            transactionState = .failed
        default:
            queue.finishTransaction(transaction)
        }
    }
}

If the respective transaction is successfully completed (.purchased), we want to set the corresponding UserDefault key to true and complete the transaction process. Also, we want to do the same if App Store Connect detects that the product has already been purchased and therefore restored (.restored). If the payment fails (.failed, .deffered), we want to print the corresponding error and also end the transaction.

So the complete function is as follows:

func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    for transaction in transactions {
        switch transaction.transactionState {
        case .purchasing:
            transactionState = .purchasing
        case .purchased:
            UserDefaults.standard.setValue(true, forKey: transaction.payment.productIdentifier)
            queue.finishTransaction(transaction)
            transactionState = .purchased
        case .restored:
            UserDefaults.standard.setValue(true, forKey: transaction.payment.productIdentifier)
            queue.finishTransaction(transaction)
            transactionState = .restored
        case .failed, .deferred:
            print("Payment Queue Error: \(String(describing: transaction.error))
                queue.finishTransaction(transaction)
                transactionState = .failed
                default:
                queue.finishTransaction(transaction)
        }
    }
}

Now we just have to define that the “Purchase” Button in our ContentView uses the purchaseProduct function of the storeManager to initialize a transaction.

Button(action: {
    storeManager.purchaseProduct(product: product)
}) {
    Text("Buy for \(product.price) $")
}

Okay, let’s see if this works by running the app on a physical device. Of course, we do not want to use our personal Apple ID for this. Instead, we use a sandbox account. Go to the iOS Settings, open “App Store” and tap on “Sign In” under “Sandbox Account”. Now use the credentials of a sandbox account you created in App Store Connect earlier to log in.

Run the app and wait until all products are loaded. To make an In-App Purchase, we tap on the respective “Purchase” button. In sandboxed mode, it may take a few seconds to start the transaction.

Tip: If your personal Apple account is still used when the transaction is initiated, try to log out of your personal iCloud account in the iOS settings and restart the app.

As soon as you are prompted to do so, use the login data of a created sandbox account to make the purchase.

If all goes well, App Store Connect will send us a message that the purchase was successful. Our ContentView is updated via the @Published transactionState property, and since a value is now stored for the UserDefault key of the corresponding product ID, the respective product is now marked as “Purchased”!

Restore products already purchased

Okay, but what if the user reinstalls the app but has already made an In-App Purchase?

Try to delete the Treasure Store App from your device and then run the app over Xcode to reinstall it. The respective product is no longer marked as “Purchased”!

Restoring purchased products is very easy. Just add the following function to the StoreManager class.

func restoreProducts() {
    print("Restoring products ...")
    SKPaymentQueue.default().restoreCompletedTransactions()
}

The “Restore Purchases” Button of our ContentView must now only call the restoreProducts function.

ToolbarItem(placement: .navigationBarTrailing) {
    Button(action: {
        storeManager.restoreProducts()
    }) {
        Text ("Restore Purchases ")
    }
}

If we now run the app again and tap on the “Restore Purchases” Button in the navigation bar, all IAPs will be restored!

Conclusion 🎊

That’s it! We have really learned a lot. You now know how to build in-app-purchases into your SwiftUI apps to monetize them.

You can download the whole source code here!

We only worked with non-consumable IAPs in this tutorial, but you should be able to apply your knowledge to other types of IAPs. Please have a look at the Apple Docs

If anything is unclear, please leave a comment!

You liked the tutorial, and you want to learn more about developing iOS apps with SwiftUI? Then check out our Interactive Mastering SwiftUI Book!

Leave a Reply

Your email address will not be published. Required fields are marked *

small_c_popup.png

Covid-19 Forces you into quarantine?

Start Mastering swiftUI Today save your 33% discount