我想执行一个延迟的任务,该任务可以被取消并在某些事件中重新创建.作为用例示例,基础模型更新得太频繁的值,但在UI上,更新仅隔一段时间显示一次.

我在互联网上看到过许多存储的TaskTask.sleep的例子,它被取消并替换为新的(例如,Sundell的Swift的this post),但我无法让它工作.

这是我正在做的事情的MRP:

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并创建一个新的.由于内部有2秒的延迟,因此我预计除了最后一次计时器迭代之外,它总是会被取消."

然而,事实并非如此:每0.5秒就会有countText次更新,就好像没有发生取消一样.

奇怪的是,如果我强制try Task.sleep,它就会抛出CancellationError,这意味着任务在延迟期间被取消.但到底为什么延迟后的代码仍然执行呢?如何正确实施?

感谢任何帮助.

UPD.在Rob回答(已接受的回答)后,我修复了示例中导致countText变量未使用的无意错误.这不影响问题的主要信息.

推荐答案

你说:

然而,事实并非如此:每0.5秒就会有countText次更新,就好像没有发生取消一样.

有几个问题:

  1. In your example, the Text isn’t showing countText, but rather "\(count)". So you are not watching countText being updated, but rather seeing count get updated. You do not appear to be using countText anywhere.

    因此,使用Text(countText)而不是Text("\(count)").

    [The已编辑问题以删除此问题.]

  2. 另外,由于您正在使用try? await Task.sleep(…),因此当task取消时,Task.sleep(…)会立即返回,但不会退出Task {…}.由于没有发现错误,因此它只会继续到Task {…}中的下一行,即更新countText的行.

    但这可能不是你的本意.如果您真的希望cancel停止整个Task {…}的执行,那么简单的解决方案是使用try而不是try?.然后,Task {…}将发现错误并立即退出,而不是继续到Task中的下一行.如果您这样做,您也会将task定义为Task<(), Error>而不是Task<(), Never>.

因此,也许:

struct ContentView: View {
    @State private var count = 0
    @State private var countText = "0"
    @State private var task: Task<(), Error>?
    
    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()
                    }
                }
            }
    }
}

这将:

  • 每0.5秒更新count;
  • 仅在countText更改时更新Text;
  • countText仅在任务未取消时才会更新;
  • 因此,它不会更新countText,直到最后一次触发计时器(因为所有其他计时都被取消了).

有一个小问题仍然存在.如果该观点被驳回怎么办?人们可以考虑用withTaskCancellationHandler来包装它.或者我想你可以在.onDisappear中取消计时器.

就我个人而言,我会更进一步,完全退休Timer人.

简单的解决方案是循环:

struct ContentView: View {
    @State private var count = 0
    @State private var countText = "0"
    
    var body: some View {
        Text(countText)
            .task {
                do {
                    for _ in 0 ..< 20 {
                        try await Task.sleep(for: .seconds(0.5))
                        count += 1
                        task?.cancel()
                        task = Task {
                            try await Task.sleep(for: .seconds(2.0))
                            countText = "\(count)"
                        }
                    }
                } catch {
                    // if this view is dismissed, this whole `.task` will get
                    // canceled automatically; but because you have employed 
                    // unstructured concurrency, we will need to cancel that 
                    // `Task` manually.

                    task?.cancel()
                }
            }
    }
}

还有其他模式(例如AsyncTimerSequence/Swift Async 算法rithms).但关键点是我们希望留在Swift并发中,而不是退回到遗留模式.


就其价值而言,我们通常建议不要从同步上下文中引入非 struct 化并发.您问题的整个前提是手动取消非 struct 化并发,所以希望我已经回答了上面的问题,但我们应该注意,这种模式是我们尽可能避免的.

别误会我的意思.非 struct 化并发具有实用性.它为我们提供了Swift并发系统中的最终控制权.但是,除其他外,考虑到它的脆弱程度,我们通常希望避免它(例如,忽略某些我们未能手动取消的执行路径是多么容易).

Swift相关问答推荐

SwiftUI加载器未覆盖UIKit导航设置中的导航栏

显示第二个操作紧接在另一个操作后的工作表在SwiftUI中不起作用

如何在AudioKit中实现自定义音效 node ?

如何在DATE()中进行比较

一种函数,用于判断变量的类型是否为在SWIFT中作为参数传递的类型

从`compactMap()`闭包返回`nil`未通过结果类型判断

Result类型的初始值设定项失败?

无论玩家移动到视图内的哪个位置,如何使用 SpriteKit 让敌人向玩家emits 子弹?

除法中如果除以完全因数需要更长时间吗?

Swift 数组拆分成重叠的块

TabView 无法在屏幕上正确显示

URL appendPathComponent(_:) 已弃用?

我如何读取并通知可可 macos 中某个类的计算(computed)属性?

使用 RxSwift 围绕 async/await 方法创建 Observable

获取具有关联类型的协议的self

是否可以使任何协议符合 Codable?

将项目引用添加到 Swift iOS XCode 项目并调试

将if let与逻辑或运算符一起使用

Swift 中惰性 var 的优势是什么

SwiftUI - 呈现工作表后导航栏按钮不可点击