了解Swift UI 中 StoreKit 2 新功能

3,496 阅读5分钟

前言

StoreKit 为我们提供了通过应用程序获得收入的机会。它允许我们设置应用内购买和订阅的购买流程。StoreKit 2 引入了一种基于现代 Swift 的 API,用于构建类型安全的应用内购买。下面我们将开始关于 StoreKit 2 的系列文章。

配置项目

首先,我们必须在项目的 “Signing & Capabilities” 选项卡中配置应用内购买项目。接下来,应该创建一个 StoreKit 配置文件,以便在没有与 App Store 的网络连接的情况下测试应用内购买功能。前往 “File -> New -> File” 并选择 “StoreKit Configuration File”。

可以创建一个仅本地的配置文件,并将其填充为测试订阅和应用内购买项目。另一种选择是启用 “Sync this file with an app in App Store Connect” 复选框,从 App Store Connect 获取订阅和应用内购买项目列表。

最后一步是使用预定义的 StoreKit 配置文件运行你的应用程序。需要编辑项目的 scheme,并在运行部分的选项标签中选择的 StoreKit 配置文件。现在,已经拥有一个完全配置的项目,允许我们在 Xcode 中测试应用内购买。

构建支付功能

让我们开始构建我们的支付功能,引入 Store 类型来处理与应用内购买相关的所有逻辑。

import StoreKit

@MainActor final class Store: ObservableObject {
    @Published private(set) var products: [Product] = []
    
    init() {}
    
    func fetchProducts() async {
        do {
            products = try await Product.products(
                for: [
                    "123456789", "987654321"
                ]
            )
        } catch {
            products = []
        }
    }
}

正如在上面的示例中所看到的,我们定义了 Store 类型,用于获取和存储将显示在支付屏幕上的产品列表。StoreKit 2 框架提供了 Product 类型,该类型封装了与应用内购买相关的所有逻辑。Product 类型具有一个名为 products 的静态函数,我们可以使用它来通过提供标识符集合来获取产品列表。

struct ContentView: View {
    @StateObject private var store = Store()
    
    var body: some View {
        VStack {
            if store.products.isEmpty {
                ProgressView()
            } else {
                ForEach(store.products, id: \.id) { product in
                    Button {
                        Task {
                            try await store.purchase(product)
                        }
                    } label: {
                        VStack {
                            Text(verbatim: product.displayName)
                                .font(.headline)
                            Text(verbatim: product.displayPrice)
                        }
                    }
                    .buttonStyle(.borderedProminent)
                }
            }
        }
        .task {
            await store.fetchProducts()
        }
    }
}

我们使用 Store 类型来获取和显示可用的应用内购买列表。Product 类型的实例包含了我们需要显示的所有信息,如应用内购买的标题、描述和价格。

Product 类型还具有 purchase 函数,我们可以使用它来启动特定产品的应用内购买流程。它返回一个 PurchaseResult 枚举的实例,定义了三种情况:成功、挂起和用户取消。

@MainActor final class Store: ObservableObject {
    // ...
    
    @Published private(set) var activeTransactions: Set<StoreKit.Transaction> = []
    
    func purchase(_ product: Product) async throws {
        let result = try await product.purchase()
        switch result {
        case .success(let verificationResult):
            if let transaction = try? verificationResult.payloadValue {
                activeTransactions.insert(transaction)
                await transaction.finish()
            }
        case .userCancelled:
            break
        case .pending:
            break
        @unknown default:
            break
        }
    }
}

每当购买结果处于成功状态时,都会提供一个 Transaction 类型的关联值,定义了成功的交易。StoreKit 将交易封装在 VerificationResult 类型中,允许我们验证交易是否正确签名并来自 App Store。

VerificationResult 类型由 StoreKit 2 用于验证数据是否有效且由 App Store 签名。它提供了 payloadValue 计算属性,我们可以使用它来解包已签名数据,或者如果数据未正确签名,则引发错误。

一旦获取了交易,应该解锁用户购买的功能,并在特定交易上调用 finish 函数。请记住,只有在解锁已购买的功能后才应该完成交易。

struct ContentView: View {
    @StateObject private var store = Store()
    
    var body: some View {
        VStack {
            Text("Purchased items: \(store.activeTransactions.count)")
            
            if store.products.isEmpty {
                ProgressView()
            } else {
                if store.activeTransactions.isEmpty {
                    ForEach(store.products, id: \.id) { product in
                        Button {
                            Task {
                                try await store.purchase(product)
                            }
                        } label: {
                            VStack {
                                Text(verbatim: product.displayName)
                                    .font(.headline)
                                Text(verbatim: product.displayPrice)
                            }
                        }
                        .buttonStyle(.borderedProminent)
                    }
                }
            }
        }
        .task {
            await store.fetchProducts()
        }
    }
}

当启用“询问购买”时,购买将变为挂起状态。在这种情况下,交易稍后才会到达,只有在父母批准后才会到达。应该观察 Transaction.updates 流来处理这种类型的交易。我们必须在应用程序启动时开始监视此流,以确保不会错过任何交易。

@MainActor final class Store: ObservableObject {
    @Published private(set) var activeTransactions: Set<StoreKit.Transaction> = []
    private var updates: Task<Void, Never>?
    
    // ...
    
    init() {
        updates = Task {
            for await update in StoreKit.Transaction.updates {
                if let transaction = try? update.payloadValue {
                    activeTransactions.insert(transaction)
                    await transaction.finish()
                }
            }
        }
    }
    
    deinit {
        updates?.cancel()
    }
}

StoreKit 2 提供了一种轻松获取所有活跃订阅和已购买产品的方法。Transaction 类型上的 currentEntitlements 属性列出了所有活跃订阅和未退款的产品。

@MainActor final class Store: ObservableObject {
    @Published private(set) var activeTransactions: Set<StoreKit.Transaction> = []
    // ...
    
    func fetchActiveTransactions() async {
        var activeTransactions: Set<StoreKit.Transaction> = []
        
        for await entitlement in StoreKit.Transaction.currentEntitlements {
            if let transaction = try? entitlement.payloadValue {
                activeTransactions.insert(transaction)
            }
        }
        
        self.activeTransactions = activeTransactions
    }
}

我们可以使用 currentEntitlements 属性来获取每次应用程序启动或更频繁时的所有活跃购买。通过主动监视 currentEntitlements 属性,我们消除了还原购买的需求,因为 currentEntitlements 始终包含最新的活跃订阅和非消耗性购买列表,即使它们是在另一台设备上购买的也是如此。

@main
struct MyApp: App {
    @Environment(\.scenePhase) private var scenePhase
    @StateObject private var store = Store()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(store)
                .task(id: scenePhase) {
                    if scenePhase == .active {
                        await store.fetchActiveTransactions()
                    }
                }
        }
    }
}

总结

这篇文章介绍了如何在 iOS 应用中使用 StoreKit 2 实现应用内购买和订阅功能。主要内容包括项目配置、构建 Paywall 功能、显示产品列表、购买产品、处理交易状态、监控交易更新和获取活跃订阅与购买。通过详细的示例和解释,开发者可以轻松了解如何利用 StoreKit 2 构建强大的应用内购买功能。