在 iOS 应用中,非消耗型内购通常用于一次性购买永久解锁的功能或内容。以下是一个使用 SwiftUI 实现非消耗型内购的简单示例。
示例场景假设我们有一个应用,用户可以通过内购永久解锁“高级功能”。
实现步骤1. 在 App Store Connect 中配置内购项目
登录 App Store Connect。
为你的应用创建一个非消耗型内购项目(例如,ID 为 com.yourapp.premium)。
2. 在 Xcode 中启用内购功能
打开 Xcode 项目。
在 Signing & Capabilities 中添加 In-App Purchase 能力。
上面的代码使用的是 StoreKit 1,而不是最新的 StoreKit 2。StoreKit 2 是 Apple 在 WWDC 2021 推出的新版本,提供了更简洁、现代化的 API,并且完全基于 Swift 的异步编程模型(async/await)。
如果你希望使用 StoreKit 2 来实现非消耗型内购,以下是更新后的代码示例:
StoreKit 2 实现非消耗型内购1. 配置内购项目
在 App Store Connect 中创建非消耗型内购项目(例如,ID 为 com.yourapp.premium)。
2. 使用 StoreKit 2 实现内购逻辑以下是基于 StoreKit 2 的 SwiftUI 实现:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147import SwiftUIimport StoreKit@MainActorclass StoreManager: ObservableObject { @Published var products: [Product] = [] @Published var isPremiumUnlocked: Bool = false private var updates: Task
StoreKit 2 的核心改进
基于 async/await 的异步编程:
StoreKit 2 完全支持 Swift 的 async/await,代码更简洁易读。
例如,Product.products(for:) 和 product.purchase() 都是异步方法。
简化的交易管理:
使用 Transaction.currentEntitlements(for:) 检查用户当前的购买状态。
使用 Transaction.updates 监听交易更新。
自动验证交易:
StoreKit 2 会自动验证交易,开发者无需手动处理收据验证。
更安全的 API:
StoreKit 2 提供了更安全的 API,减少开发者的错误使用。
代码说明
StoreManager:
使用 @MainActor 确保 UI 更新在主线程执行。
通过 Product.products(for:) 获取内购产品。
使用 product.purchase() 发起购买。
通过 Transaction.currentEntitlements(for:) 检查用户是否已购买。
ContentView:
显示 UI,包括购买按钮和恢复购买按钮。
根据 isPremiumUnlocked 状态显示不同的内容。
UserDefaults:
用于持久化存储用户是否已解锁高级功能。
运行流程
应用启动时,检查是否已解锁高级功能。
显示购买按钮,用户可以点击购买。
购买成功后,解锁高级功能并更新 UI。
用户可以通过“恢复购买”按钮恢复之前的购买。
注意事项
在真实应用中,建议将购买状态存储在服务器或使用更安全的方式(如 Keychain)。
测试时,使用沙盒账户进行内购测试。
确保在 App Store Connect 中正确配置内购项目。
如果你有其他问题,欢迎随时提问!
当然!以下是对你提供的代码的详细分析。我们将从代码结构、功能实现、潜在问题和改进建议等方面进行探讨。
代码结构分析1. StoreManager 类
职责:管理内购逻辑,包括获取产品、处理购买、恢复购买和检查购买状态。
关键属性:
products: [Product]:存储从 App Store 获取的内购产品。
isPremiumUnlocked: Bool:表示用户是否已解锁高级功能。
关键方法:
fetchProducts():异步获取内购产品。
purchase(product:):发起购买请求。
restorePurchases():恢复购买。
checkUnlockedStatus():检查用户是否已解锁高级功能。
unlockPremium():解锁高级功能并更新状态。
observeTransactionUpdates():监听交易更新。
2. ContentView 视图
职责:显示 UI,包括购买按钮和恢复购买按钮。
关键逻辑:
根据 isPremiumUnlocked 状态显示不同的内容。
调用 StoreManager 的方法处理用户操作。
3. UserDefaults
用途:持久化存储用户是否已解锁高级功能。
功能实现分析1. 获取内购产品
通过 Product.products(for:) 异步获取内购产品。
将获取到的产品存储在 products 数组中。
2. 发起购买
使用 product.purchase() 发起购买请求。
处理购买结果:
如果购买成功,调用 unlockPremium() 解锁高级功能。
如果购买失败或用户取消,打印错误信息。
3. 恢复购买
使用 AppStore.sync() 同步用户的购买记录。
调用 checkUnlockedStatus() 检查用户是否已解锁高级功能。
4. 检查购买状态
使用 Transaction.currentEntitlements(for:) 检查用户当前的购买状态。
如果找到有效的交易记录,解锁高级功能。
5. 监听交易更新
使用 Transaction.updates 监听交易更新。
如果交易状态发生变化(例如退款或撤销),更新购买状态。
潜在问题分析1. UserDefaults 的安全性
问题:UserDefaults 不是存储敏感数据(如购买状态)的安全方式。
建议:使用更安全的方式(如 Keychain)存储购买状态。
2. 未处理网络错误
问题:在 fetchProducts() 和 purchase(product:) 中,网络错误可能导致功能异常。
建议:添加更详细的错误处理逻辑,并向用户显示友好的错误提示。
3. 未处理退款或撤销
问题:如果用户退款或撤销购买,代码中没有显式处理这种情况。
建议:在 observeTransactionUpdates() 中检查 transaction.revocationDate,并根据需要锁定高级功能。
4. 未处理多产品场景
问题:代码假设只有一个内购产品(com.yourapp.premium),无法处理多个产品。
建议:扩展代码以支持多个产品,并根据产品 ID 动态更新 UI。
5. 未处理沙盒测试
问题:代码中没有明确区分沙盒环境和生产环境。
建议:在测试时,确保使用沙盒账户,并添加日志以帮助调试。
改进建议1. 使用 Keychain 存储购买状态
替换 UserDefaults,使用 Keychain 存储购买状态,提高安全性。
2. 增强错误处理
在 fetchProducts() 和 purchase(product:) 中添加详细的错误处理逻辑。
向用户显示友好的错误提示。
3. 处理退款或撤销
在 observeTransactionUpdates() 中检查 transaction.revocationDate,并根据需要锁定高级功能。
4. 支持多产品
扩展代码以支持多个内购产品,并根据产品 ID 动态更新 UI。
5. 添加沙盒测试支持
在测试时,确保使用沙盒账户,并添加日志以帮助调试。
改进后的代码示例以下是改进后的部分代码:
使用 Keychain 存储购买状态123456789101112131415161718192021222324252627import Securityfunc saveToKeychain(key: String, value: Bool) { let data = Data(String(value).utf8) let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecValueData as String: data ] SecItemDelete(query as CFDictionary) SecItemAdd(query as CFDictionary, nil)}func loadFromKeychain(key: String) -> Bool { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecReturnData as String: kCFBooleanTrue!, kSecMatchLimit as String: kSecMatchLimitOne ] var dataTypeRef: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef) if status == errSecSuccess, let data = dataTypeRef as? Data, let value = String(data: data, encoding: .utf8) { return Bool(value) ?? false } return false}
增强错误处理1234567891011121314151617181920212223func purchase(product: Product) { Task { do { let result = try await product.purchase() switch result { case .success(let verification): if case .verified(let transaction) = verification { await transaction.finish() unlockPremium() } case .pending: print("Purchase is pending (e.g., waiting for parental approval)") case .userCancelled: print("User cancelled the purchase") @unknown default: break } } catch { print("Purchase failed: \(error)") // 显示友好的错误提示 } }}
总结你的代码已经实现了基本的内购功能,但可以通过以下方式进一步改进:
提高购买状态存储的安全性。
增强错误处理和用户提示。
支持多产品和处理退款/撤销。
优化沙盒测试支持。
如果你有其他问题或需要进一步的帮助,欢迎随时提问!