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

Swift中如何中断for-await循环的等待状态?

如何中断StoreKit2中Transaction.updates的for-await循环等待状态

针对你遇到的问题——调用Task.cancel()无法终止Transaction.updates的for-await循环(因为序列在等待新交易时不会响应取消),这里有几个可行的解决方案,尤其适合单元测试场景:

方案1:用AsyncStream包装Transaction.updates,添加取消响应

核心思路是给不响应取消的Transaction.updates套一层能监听Task取消状态的AsyncStream,一旦Task被取消,就主动结束序列,终止循环。

先写一个通用的扩展方法,给任意AsyncSequence添加取消支持:

extension AsyncSequence {
    func makeCancellable() -> AsyncStream<Element> {
        AsyncStream { continuation in
            // 启动子Task遍历原序列
            let sequenceTask = Task {
                do {
                    for await element in self {
                        continuation.yield(element)
                        // 每次产出元素后检查取消状态
                        if Task.isCancelled {
                            break
                        }
                    }
                    continuation.finish()
                } catch {
                    continuation.finish(throwing: error)
                }
            }

            // stream终止时取消子Task
            continuation.onTermination = { _ in
                sequenceTask.cancel()
            }

            // 监听当前Task的取消状态,触发时终止stream和子Task
            Task {
                await Task.yield()
                if Task.isCancelled {
                    sequenceTask.cancel()
                    continuation.finish()
                }
            }
        }
    }
}

修改你的监听代码:

transactionListener = Task(priority: .background) { [self] in
    for await verificationResult in Transaction.updates.makeCancellable() {
        // --- 处理交易逻辑 ---
        await verificationResult.transaction.finish()
    }
    print("done")
}

现在调用transactionListener.cancel()时,包装的AsyncStream会立即结束,for-await循环也会随之终止,不会卡在等待状态。

方案2:单元测试中使用测试替身(Test Double)

如果只是单元测试场景的问题,更干净的做法是给Transaction.updates替换成测试专用的AsyncSequence,完全控制序列的结束时机,避免依赖真实StoreKit API。

定义测试用序列:

class MockTransactionUpdates: AsyncSequence {
    typealias Element = VerificationResult<Transaction>
    
    private let elements: [Element]
    private let shouldFinish: AsyncStream<Void>
    
    init(elements: [Element], shouldFinish: AsyncStream<Void>) {
        self.elements = elements
        self.shouldFinish = shouldFinish
    }
    
    func makeAsyncIterator() -> AsyncIterator {
        AsyncIterator(elements: elements.makeIterator(), shouldFinish: shouldFinish.makeAsyncIterator())
    }
    
    struct AsyncIterator: AsyncIteratorProtocol {
        var elements: IndexingIterator<[Element]>
        var shouldFinish: AsyncStream<Void>.AsyncIterator
        
        mutating func next() async -> Element? {
            // 同时等待元素产出或结束信号
            let result = await Task.select(
                Task { elements.next() },
                Task { await shouldFinish.next(); return nil }
            )
            return result
        }
    }
}

测试中控制序列结束:

// 测试setup
var finishStream: AsyncStream<Void>!
var finishContinuation: AsyncStream<Void>.Continuation!
finishStream = AsyncStream { finishContinuation = $0 }
let mockUpdates = MockTransactionUpdates(elements: [], shouldFinish: finishStream)
// 替换Transaction.updates为mockUpdates(需通过依赖注入实现)

// 测试teardown
finishContinuation.yield()
transactionListener.cancel()

这种方法能彻底隔离测试环境,避免残留监听器影响后续测试。

方案3:用Task.select结合取消检查

如果不想写包装扩展,可以在循环内部用Task.select同时等待交易更新和取消信号,触发取消时直接退出循环:

transactionListener = Task(priority: .background) { [self] in
    // 创建监听取消的AsyncStream
    let cancelSignal = AsyncStream<Void> { continuation in
        Task {
            await Task.yield()
            if Task.isCancelled {
                continuation.yield()
                continuation.finish()
            }
        }
    }
    
    let updatesIterator = Transaction.updates.makeAsyncIterator()
    let cancelIterator = cancelSignal.makeAsyncIterator()
    
    while !Task.isCancelled {
        let result = await Task.select(
            Task { try? await updatesIterator.next() },
            Task { await cancelIterator.next() }
        )
        
        switch result {
        case .first(let transactionResult):
            guard let verificationResult = transactionResult else { break }
            // --- 处理交易 ---
            await verificationResult.transaction.finish()
        case .second:
            // 收到取消信号,退出循环
            break
        }
    }
    print("done")
}

这种方法无需额外扩展,直接在循环内部处理取消逻辑,适合临时场景。

内容的提问来源于stack exchange,提问作者jboi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 09:15:35