基于StoreKit 2实现订阅取消/到期自动监听的优化问询
问题
使用StoreKit 2实现订阅功能时,遇到订阅过期或取消后,应用无法在无用户交互的情况下自动更新订阅状态的问题。当前前台运行时无法自动捕获订阅状态变更,临时方案是添加30秒延迟计时器,希望得到更优雅的实现方式。
现有实现代码
管理器启动代码
public func start(productIds: [String]) { updates = observeTransactionUpdates() Task { do { self.storeKitProducts = try await Product.products(for: productIds) await updateUserSubscriptions() } catch { print("Error retrieving Products: \(error)") } } }
购买逻辑代码
let result = try await storeKitProduct.purchase() switch result { case .success(.verified(let transaction)): await updateUserSubscriptions() await transaction.finish() default: break }
订阅更新监听代码
private func observeTransactionUpdates() -> Task<Void, Error> { Task(priority: .background) { [unowned self] in for await result in Transaction.updates { guard case .verified(let transaction) = result else { return } await updateUserSubscriptions() await transaction.finish() } } }
updateUserSubscriptions 实现
private func updateUserSubscriptions() async { var product: Product? for await result in Transaction.currentEntitlements { guard case .verified(let transaction) = result else { continue } if transaction.revocationDate == nil { product = products.first(where: { $0.productID == transaction.productID }) } await transaction.finish() } updateSubscribedProduct(product) }
updateSubscribedProduct 方法
private func updateSubscribedProduct(_ subscribedProduct: Product?) { self.subscribedProduct = subscribedProduct addSubscriptionExpiration() let previousIsSubscribed = isSubscribed isSubscribed = subscribedProduct != nil if previousIsSubscribed != isSubscribed { subscriptionChanged.send(isSubscribed) } }
临时计时器方案
private func addSubscriptionExpiration() { Task { guard let product, let result = await Transaction.currentEntitlement(for: product.productID), case .verified(let transaction) = result, let expirationDate = transaction.expirationDate else { return } let newDate = expirationDate.addingTimeInterval(30) timer = Timer(fire: newDate, interval: 0, repeats: false) { [weak self] _ in guard let self else { return } Task { if await !self.isSubscriptionValid { await self.updateUserSubscriptions() } } } RunLoop.main.add(timer!, forMode: .common) } }
优化方案
1. 修复 Transaction.updates 监听漏洞
Transaction.updates仅监听主动交易变更(如用户主动取消、退款、续订),订阅到期属于被动状态变更,不会触发该流。同时优化监听逻辑:
- 用
weak self替代unowned self,避免循环引用风险 - 处理未验证的交易,记录错误便于排查
修改后的监听代码:
private func observeTransactionUpdates() -> Task<Void, Never> { Task(priority: .background) { [weak self] in for await result in Transaction.updates { switch result { case .verified(let transaction): await self?.updateUserSubscriptions() await transaction.finish() case .unverified(let transaction, let error): print("Transaction verification failed: \(error)") await transaction.finish() } } } }
2. 用Swift Concurrency替代传统Timer
使用Task.sleep实现异步到期检查,更符合现代异步编程范式,避免RunLoop相关问题:
首先添加属性保存检查任务:
private var expirationCheckTask: Task<Void, Never>?
然后实现异步到期检查逻辑:
private func scheduleSubscriptionExpirationCheck(for product: Product) async { guard let result = await Transaction.currentEntitlement(for: product.productID), case .verified(let transaction) = result, let expirationDate = transaction.expirationDate else { return } let now = Date() let timeUntilCheck = expirationDate.timeIntervalSince(now) + 30 guard timeUntilCheck > 0 else { await updateUserSubscriptions() return } try? await Task.sleep(nanoseconds: UInt64(timeUntilCheck * 1_000_000_000)) guard !Task.isCancelled else { return } await updateUserSubscriptions() }
修改updateSubscribedProduct方法,替换原计时器调用:
private func updateSubscribedProduct(_ subscribedProduct: Product?) { self.subscribedProduct = subscribedProduct expirationCheckTask?.cancel() if let product = subscribedProduct { expirationCheckTask = Task { await scheduleSubscriptionExpirationCheck(for: product) } } let previousIsSubscribed = isSubscribed isSubscribed = subscribedProduct != nil if previousIsSubscribed != isSubscribed { subscriptionChanged.send(isSubscribed) } }
3. 补充前后台切换时的状态刷新
应用从后台回到前台时,主动刷新订阅状态,确保状态最新:
添加通知监听逻辑:
private func setupForegroundRefresh() { NotificationCenter.default.addObserver( self, selector: #selector(handleAppEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil ) } @objc private func handleAppEnterForeground() { Task { await updateUserSubscriptions() } }
在start方法中调用setupForegroundRefresh()完成初始化。
4. 优化 updateUserSubscriptions 逻辑
当前遍历currentEntitlements时会覆盖product变量,单订阅场景下可提前退出循环提升效率:
private func updateUserSubscriptions() async { var activeProduct: Product? for await result in Transaction.currentEntitlements { switch result { case .verified(let transaction): if transaction.revocationDate == nil, let product = products.first(where: { $0.productID == transaction.productID }) { activeProduct = product break // 单订阅场景找到有效订阅后提前退出 } await transaction.finish() case .unverified(_, let error): print("Current entitlement verification failed: \(error)") } } updateSubscribedProduct(activeProduct) }
内容的提问来源于stack exchange,提问作者scarpz
相关产品推荐
相关产品推荐

