You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 21:05:13