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:
- Fetching products from the App Store
- Processing transactions
- Checking for existing purchases
- Listening for new transactions
- 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:
- Products configured in App Store Connect, or in StoreKit Configuration file for testing in Xcode. Check out this article I wrote to set up StoreKit configuration for testing in Xcode;
- Basic understanding of state management in SwiftUI with
Combineframework’s@ObservableObject; - Familiarity with Swift’s async-await concurrency. We won’t be talking about it, but we sure will be using it a lot during implementation.
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()
}
View complete code for StoreKit implementation.
import StoreKit
enum Products: String, CaseIterable {
case refillFood, refillWater, petCat, petDog
}
class Store: ObservableObject {
@Published var products: [Product] = []
@Published var purchasedProducts: [Product] = []
private let productIDs: Set<String> = {
var result = Set<String>()
for product in Products.allCases {
result.insert(product.rawValue)
}
return result
}()
private var updates: Task<Void, Never>? = nil
init() {
listenForUpdates()
Task {
await fetchProducts()
await refreshPurchasedProducts()
}
}
deinit {
updates?.cancel()
}
@MainActor
private func fetchProducts() async {
do {
let loadedProducts = try await Product.products(for: productIDs)
products = loadedProducts
} catch {
// Handle errors here...
}
}
private func verify(_ result: VerificationResult<Transaction>) throws -> Transaction {
switch result {
case .unverified(_, let verificationError):
throw verificationError
case .verified(let transaction):
return transaction
}
}
@MainActor
private 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
}
}
}
@MainActor
private func refreshPurchasedProducts() async {
for await result in Transaction.currentEntitlements {
do {
let transaction = try verify(result)
process(transaction)
} catch {
// Handle errors here...
}
}
}
private 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...
}
}
}
}
}
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.

