Swift中可取消延迟任务的实现异常问题排查
我需要执行一个可取消且能在特定事件下重新创建的延迟任务,例如:底层模型更新过于频繁,但UI仅需间隔一段时间展示更新内容。
我在网上看到很多存储Task并在内部使用Task.sleep的示例,通过取消并替换新Task实现需求,但无法成功运行。
以下是我的最小可复现代码:
struct PlaygroundView: View { var body: some View { Text("\(countText)") .task { Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { task?.cancel() task = Task { try? await Task.sleep(for: .seconds(2.0)) countText = "\(count)" } count += 1 if count == 20 { $0.invalidate() } } } } @State private var count = 0 @State private var countText = "0" @State private var task: Task<(), Never>? }
定时器每0.5秒触发一次,取消之前创建的Task并创建新Task。由于内部有2秒延迟,我预期除最后一次定时器迭代外,其他Task都会被取消。
但实际情况是:countText每0.5秒更新一次,仿佛没有执行取消操作。
奇怪的是,如果强制尝试Task.sleep,会抛出CancellationError,说明任务在延迟期间已被取消,但为何延迟后的代码仍会执行?该如何正确实现?
更新:在修复了示例中countText未被正确使用的无心错误后,问题核心仍存在。
问题根源
你的代码中,try? await Task.sleep(for: .seconds(2.0))会吞掉所有错误,包括任务被取消时抛出的CancellationError。这意味着无论Task是否被取消,sleep之后的countText = "\(count)"都会执行——因为错误被忽略了,任务不会提前终止。
即使你改用try await并抛出CancellationError,如果Task的类型是Task<(), Never>(不允许错误),错误会被隐式丢弃,任务依然会执行后续代码;只有当Task类型为Task<(), Error>时,未处理的错误才会终止任务,但你的代码用了try?,所以还是会忽略错误。
正确实现方案
方案1:检查任务取消状态
在Task.sleep之后,显式判断当前任务是否已取消,取消则跳过UI更新:
struct PlaygroundView: View { var body: some View { Text(countText) .task { Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in task?.cancel() task = Task { try? await Task.sleep(for: .seconds(2.0)) // 任务已取消则直接返回,不执行UI更新 guard !Task.isCancelled else { return } countText = "\(count)" } count += 1 if count == 20 { timer.invalidate() } } } } @State private var count = 0 @State private var countText = "0" @State private var task: Task<(), Never>? }
方案2:捕获取消错误并终止任务
改用try await并捕获CancellationError,在任务取消时提前退出:
struct PlaygroundView: View { var body: some View { Text(countText) .task { Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in task?.cancel() // 将Task类型改为允许抛出错误 task = Task { do { try await Task.sleep(for: .seconds(2.0)) countText = "\(count)" } catch is CancellationError { // 任务被取消,直接终止 return } catch { // 处理其他潜在错误 } } count += 1 if count == 20 { timer.invalidate() } } } } @State private var count = 0 @State private var countText = "0" @State private var task: Task<(), Error>? }
方案3:更优雅的防抖实现(推荐)
利用SwiftUI的并发特性和发布者,替代手动管理Timer和Task,更符合SwiftUI的设计习惯:
struct PlaygroundView: View { @State private var count = 0 @State private var countText = "0" @State private var updateTask: Task<(), Never>? var body: some View { Text(countText) // 用Timer发布者替代scheduledTimer,更贴合SwiftUI数据流 .onReceive(Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()) { _ in count += 1 triggerDebouncedUpdate() if count == 20 { updateTask?.cancel() } } } private func triggerDebouncedUpdate() { // 取消之前的延迟任务 updateTask?.cancel() // 创建新的延迟任务 updateTask = Task { try? await Task.sleep(for: .seconds(2.0)) guard !Task.isCancelled else { return } // 显式切换到主线程更新UI,确保线程安全 await MainActor.run { countText = "\(count)" } } } }
内容的提问来源于stack exchange,提问作者lazarevzubov

