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
相关产品推荐
相关产品推荐

