---
title: "In-app purchases initialization with Swift"
description: "Learn how to optimize your SKProduct listings for iOS and provide seamless restore purchase functionality to your users. This article provides Storekit restore purchases examples to help you improve the user experience and increase revenue for your app."
url: "https://adapty.io/blog/product-optimization-for-ios/"
language: "en"
slug: "product-optimization-for-ios"
category: "adapty-updates"
date: "2020-10-02T00:00:00.000Z"
date_modified: "2025-08-25T12:05:12.000Z"
author: "Ben Gohlke"
---

# In-app purchases initialization with Swift

## TL;DR

The 2nd tutorial of our series on implementing in-app purchases on iOS. In this part, we'll show you how to create a simple paywall as well as how to initialize and process the products we've previously configured.

In our second installment, we continue our series dedicated to **iOS in-app purchases**. In the previous part, we discussed the process of creating in-app purchases and their configuration. You can read all the tutorials following the links below:

1. [iOS in-app purchases, part 1: App Store Connect and project configuration](https://adapty.io/blog/how-to-set-up-ios-in-app-purchases/)
2. iOS in-app purchases, part 2: initialization and purchases processing
3. [iOS in-app purchases, part 3: testing purchases in Xcode](https://adapty.io/blog/how-to-test-ios-in-app-purchases/)
4. [iOS in-app purchases, part 4: server-side receipt validation](https://adapty.io/blog/ios-in-app-purchase-server-side-validation/)
5. [iOS in-app purchases, part 5: the list of SKError codes and how to handle them](https://adapty.io/blog/ios-skerrordomain-error-codes/)

In this part, we'll show you how to create a simple paywall as well as how to initialize and process the products we've previously configured.

## Creating an in-app subscription screen

Any app that uses in-app purchases has a paywall. Apple provides some [guidelines](https://developer.apple.com/design/human-interface-guidelines/subscriptions/overview/) that determine must-have elements. At this step, we’ll implement just some of them to give you a sense of that to do next.

![MovieManiaPaywall Simple 501x1024](https://adapty.io/wp-content/uploads/2025/05/MovieManiaPaywall-Simple.png)

Our screen will have the following:

- Header with premium description
- Subscription activation buttons with local currency and title
- Restore purchases button. This element is necessary for all the applications that use subscriptions or non-consumable purchases.

I've used SwiftUI to build a simple Paywall view. Your paywall view will likely be more complex, but for purposes of this article, we're keeping it simple.

## Show purchase information using Product

After quickly familiarizing yourself with [Apple's documentation on the Product type](https://developer.apple.com/documentation/storekit/product), let's build our PaywallView. It receives basic product info from the `IAPSubscriptionManager`, but there isn't any purchasing logic yet. We'll add that soon.

```swift
import SwiftUI
import StoreKit

struct PaywallView: View {
  @Environment(IAPSubscriptionManager.self) var subMgr
  @Environment(\\.dismiss) var dismiss

  var body: some View {
    VStack {
      Text("Upgrade to")
        .foregroundStyle(.secondary)
        .font(.title2.weight(.semibold))
        .padding(.top)
      
      Text("Movie Mania Pro!")
        .foregroundStyle(.purple)
        .font(.largeTitle.bold())
      
      Spacer()
      
      ForEach(subMgr.subscriptions) { product in
        Button {
          
        } label: {
          VStack {
            Text(product.displayName)
            
            Text(product.displayPrice)
              .font(.subheadline.bold())
          }
        }
        .padding(.horizontal)
        .buttonStyle(
          BlockButtonStyle(
            textColor: .white,
            bgColor: .purple
          )
        )
      }
      
      Button("Restore Purchases") {
        
      }
      .padding(.top)
    }
  }
}
```

Let's see what's inside:

1. An environment variable that connects to our injected subscription manager.
2. Some text views to display the copy at the top.
3. A `ForEach` that displays the buttons.
4. A restore button which is a requirement when presenting non consumables or subscriptions.

This button style will let you easily create the purchase buttons with standard formatting and styling.

```swift
struct BlockButtonStyle: ButtonStyle {
  let textColor: Color
  let bgColor: Color
  
  func makeBody(configuration: Configuration) -> some View {
    HStack {
      configuration.label
        .foregroundColor(textColor)
        .font(.headline)
    }
    .padding()
    .frame(maxWidth: .infinity)
    .background(bgColor.cornerRadius(12))
    .scaleEffect(configuration.isPressed ? 0.95 : 1)
  }
}
```

## Subscription Manager

The `IAPSubscriptionManager` provides a nice interface to all things related to the products and managing their purchase by a user. Right now it only provides the products for display, but we'll add to this as we go. The type aliases are simply to make it easier to reference the related APIs.

```swift
import Foundation
import OSLog
import StoreKit

typealias Transaction = StoreKit.Transaction
typealias SubStatus = StoreKit.Product.SubscriptionInfo.Status
typealias RenewalInfo = StoreKit.Product.SubscriptionInfo.RenewalInfo
typealias RenewalState = StoreKit.Product.SubscriptionInfo.RenewalState

@Observable
final class IAPSubscriptionManager {
  private(set) var subscriptions: [Product] = []
  private(set) var currentSubscription: Product?
  private(set) var status: SubStatus?
  
  private let logger = Logger(subsystem: "io.adapty.MovieMania", category: "IAP Helper Service")
  private let identifiers: [String] = ["movie.mania.pro.1y", "movie.mania.pro.1m"]
  
  @MainActor
  func getProducts() async {
    do {
      subscriptions = try await Product.products(for: identifiers)
    } catch {
      logger.error("Products could not be fetched: \\(error.localizedDescription)")
    }
  }
}
```

## Making a Purchase

Let's add some error handling to our code. I've created a `StoreError` enum to handle error cases. It implements the `Error` protocol to interoperate with error handling code.

```swift
enum StoreError: Error {
  case failedVerification
  case purchaseInProgress
  case productNotFound
  case unknown
}
```

All errors on the StoreKit level will be caught as a single error. To get more information about the error types, check the [documentation](https://developer.apple.com/documentation/storekit/skerror).

The `purchase(_:)` function allows you to process a purchase request for a particular subscription product. It attempts to process the purchase, and handles the result returned.

Add this function to your `IAPSubscriptionManager`:

```swift
@MainActor
func purchase(_ product: Product) async throws -> Bool {   
  let result = try await product.purchase()
    
  switch result {
    case .success(let verificationResult): // 1
      switch verificationResult {
        case .verified(let transaction): // 2
          //Deliver content to the user.
          await updateSubscriptionStatus(with: transaction)
            
          //Always finish a transaction.
          await transaction.finish()
            
          return true
        case .unverified: // 3
          throw StoreError.failedVerification
      }
    case .userCancelled, .pending: // 4
      // account verification needed, or parental approval needed
      return false
    @unknown default:
      return false
  }
}
```

Let's dig into the above to understand what's going on:

1. Most times, you get a successful purchase, which will provide an associated verification result.
2. If the purchase comes back verified, it means the StoreKit automatic verification checks passed. You should unlock premium functionality at this point.
3. If the purchase is unverified, we throw the `failedVerification` error message.
4. If the user canceled the request, or a child device is pending verification of the purchase from a parent's device, you might get this result.

To process the result of the purchase, we need to implement the `updateSubscriptionStatus` method:

```swift
@MainActor
func updateSubscriptionStatus(with transaction: Transaction?) async {
  do { // 1
    guard let product = subscriptions.first,
          let statuses = try await product.subscription?.status else {
      return
    }
    
    // 2
    for status in statuses {
      switch status.state {
        case .expired, .revoked:
          continue
        default: // 3
          let renewalInfo = try checkVerified(status.renewalInfo)
          
          // 4
          guard let foundSubscription = subscriptions.first(where: { $0.id == renewalInfo.currentProductID }) else {
            continue
          }
          
          // 5
          self.status = status
          currentSubscription = foundSubscription
          break
      }
    }
  } catch {
    logger.error("Could not update subscription status: \\(error.localizedDescription)")
  }
}

// 6
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
  // Check if the transaction passes StoreKit verification.
  switch result {
    case .unverified:
      // StoreKit has parsed the JWS but failed verification. Don't deliver content to the user.
      throw StoreError.failedVerification
    case .verified(let safe):
      // If the transaction is verified, unwrap and return it.
      return safe
  }
}
```

1. Since we only have one subscription group, the status returned applies to every subscription in the group.
2. You may receive multiple statuses if the subscription is part of family sharing.
3. Once you find a status that isn't revoked or expired, extract the `renewalInfo` from the `verificationResult`.
4. Match the `currentProductID` in the `renewalInfo` with a product ID from your list of subscriptions.
5. Finally, update your properties to store the status, and the active subscription.
6. The `checkVerified(_:)` function allows you to verify the `renewalInfo` and unwrap its contents.

## Listening for Purchases Made

The last step inside the subscription manager is to set up a listener for transactions that happen outside the context of the purchase function. This step is heavily recommended by Apple and you'll actually get a runtime warning if you don't implement it.

I've posted the complete `IAPSubscriptionManager` class below to allow you to see everything we've built so far. I'll call out the changes to implement the listener underneath the code block.

```bash
import Foundation
import OSLog
import StoreKit

typealias Transaction = StoreKit.Transaction
typealias SubStatus = StoreKit.Product.SubscriptionInfo.Status
typealias RenewalInfo = StoreKit.Product.SubscriptionInfo.RenewalInfo
typealias RenewalState = StoreKit.Product.SubscriptionInfo.RenewalState

enum StoreError: Error {
  case failedVerification
  case purchaseInProgress
  case productNotFound
  case unknown
}

@Observable
final class IAPSubscriptionManager {
  private(set) var subscriptions: [Product] = []
  private(set) var currentSubscription: Product?
  private(set) var status: SubStatus?
  
  private let logger = Logger(subsystem: "io.adapty.MovieMania", category: "IAP Helper Service")
  private let identifiers: [String] = ["movie.mania.pro.1y", "movie.mania.pro.1m"]
  
  // 1
  var updates: Task<Void, Error>? = nil
  
  // 2
  init() {
    updates = newTransactionsListenerTask()
    
    Task {
      await getProducts()
      await updateSubscriptionStatus()
    }
  }
  
  // 3
  deinit {
    updates?.cancel()
  }
    
  // 4
  private func newTransactionsListenerTask() -> Task<Void, Error> {
    Task(priority: .background) {
      for await verificationResult in Transaction.updates {
        self.handle(updatedTransaction: verificationResult)
      }
    }
  }
  
  // 5
  private func handle(updatedTransaction verificationResult: VerificationResult<Transaction>) {
    guard case .verified(let transaction) = verificationResult else {
      return
    }
    
    Task {
      await updateSubscriptionStatus()
      await transaction.finish()
    }
  }
  
  @MainActor
  func getProducts() async {
    do {
      subscriptions = try await Product.products(for: identifiers)
    } catch {
      logger.error("Products could not be fetched: \\(error.localizedDescription)")
    }
  }
  
  @MainActor
  func purchase(_ product: Product) async throws -> Bool {
    let result = try await product.purchase()
    
    switch result {
      case .success(let verificationResult):
        switch verificationResult {
          case .verified(let transaction):
            // Deliver content to the user.
            await updateSubscriptionStatus()
            
            // Always finish a transaction.
            await transaction.finish()
            
            return true
          case .unverified:
            throw StoreError.failedVerification
        }
      case .userCancelled, .pending:
        // account verification needed, or parental approval needed
        return false
      @unknown default:
        return false
    }
  }
  
  @MainActor
  func updateSubscriptionStatus() async {
    do {
      /// Since we only have 1 subscription group, the status returned applies to every sub in the group.
      guard let product = subscriptions.first,
            let statuses = try await product.subscription?.status else {
        return
      }
      
      for status in statuses {
        switch status.state {
          case .expired, .revoked:
            continue
          default:
            let renewalInfo = try checkVerified(status.renewalInfo)
            
            guard let foundSubscription = subscriptions.first(where: { $0.id == renewalInfo.currentProductID }) else {
              continue
            }
            
            self.status = status
            currentSubscription = foundSubscription
            break
        }
      }
    } catch {
      logger.error("Could not update subscription status: \\(error.localizedDescription)")
    }
  }
  
  func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
    switch result {
      case .unverified:
        throw StoreError.failedVerification
      case .verified(let safe):
        return safe
    }
  }
}
```

1. This is the task that will run and listen for updates to subscriptions.
2. The `init()` function accomplishes three things: start up the listener task, fetch the products, and update the subscription status. The last step helps to ensure functionality is unlocked due to a previous subscription purchase.
3. `deinit()` ensures the task is killed when the app closes.
4. `newTransactionsListenerTask()` sets up a listener task to listen for transactions from StoreKit that happen outside the context of the purchase method.
5. `handle(updatedTransaction:)` processes any received transactions fro the listener in much the same way that the purchase method works.

## Wire the Button to the Purchase Process

All that's left is to wire up the button's action closure to call the purchase method from our subscription manager and handle the result. Edit your button in the Paywall.swift file to look like the following:

```swift
Button {
  Task {
    do {
      let success = try await subMgr.purchase(product)
      if success {
        dismiss()
      }
    } catch {
      print("Error purchasing product: \\(error.localizedDescription)")
    }
  }
} label: {
  VStack {
    Text(product.displayName)
    
    Text(product.displayPrice)
      .font(.subheadline.bold())
  }
}
```

We now have a functioning basic paywall. In the next part, we will discuss the ways to test the purchase mechanism, and [how to design a paywall](https://adapty.io/blog/how-to-design-paywall-to-pass-review-for-app-store/) to pass the App Store review.
