[SwiftUI 100 天] 用定时器重复触发事件

1,135 阅读4分钟

译自 www.hackingwithswift.com/books/ios-s…

更多内容,欢迎关注公众号 「Swift花园」

喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀

用定时器重复触发事件

iOS 内建了 Timer 类,可以让我们基于规律的时间点运行代码,其中用到了一个来自 Apple 的 Combine 框架的 publisher 系统。在这个 100 天系列中,我们实际上在许多个应用中都使用了 Combine 框架里部分功能,只不过你可能没有注意到。例如,@Published 属性包装器和 ObservableObject 协议都是来自 Combine,但我们不需要知道这一点。因为当你导入 SwiftUI 时,其实隐式导入了部分 Combine 的东西。

Apple 的一个核心系统库是 Foundation,它提供了诸如 DataDateNSSortDescriptorUserDefaults 之类的东西,也提供了像 Timer 这样的类。它被设计来在特定秒数之后执行代码,并且可以重复执行。Combine 给它添加了一个扩展,使得定时器变成发布者。发布者是一种会在值改变时对外宣告的东西。这也正是 @Published 属性包装器名称的由来,定时器发布者的工作机制完全相同:当你的时间间隔到了,Combine 会发布通知,包含当前的日期和时间。

创建定时器发布者的代码如下:

let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

上面这行代码做了几件事:

  1. 要求定时器每秒发射一次。
  2. 要求定时器运行在主线程。
  3. 要求定时器运行 common loop,这个 loop 也是你最常用的。
  4. 立即连接定时器,也就是说,定时器立即开始计时。
  5. 把定时器赋给 timer 常量。

如果你还记得,在项目 7 中我说过 “@Published 或多或少相当于半个 @State” —— 它发送变化的通知给监视它的其他东西。对于像上面这个常规的发布者,我们需要借助 onReceive() modifier 手工捕获通知。这个 modifier 接收一个发布者作为第一个参数,然后是一个闭包作为第二个参数。它会在发布者每次发送通知时指定闭包。

对于我们的例子,可以这样接收通知:

Text("Hello, World!")
    .onReceive(timer) { time in
        print("当前时间是 \(time)")
    }

这会每秒都打印出当前时间,直到定时器停止工作。

说到停止定时器,停止我们创建的这个定时器需要一些探究。你看,我们创建的 timer 属性是一个自动连接的发布者,因此我们需要回到它的上游发布者来找到定时器本身,然后取消掉它。老实说,如果没有代码自动补全,下面的代码可能很难自行发现:

self.timer.upstream.connect().cancel()

例如,我们可以更新之前的代码,让定时器每 5 秒才发射一次,代码如下:

struct ContentView: View {
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    @State private var counter = 0

    var body: some View {
        Text("Hello, World!")
            .onReceive(timer) { time in
                if self.counter == 5 {
                    self.timer.upstream.connect().cancel()
                } else {
                    print("当前时间是 \(time)")
                }

                self.counter += 1
            }
    }
}

在结束之前,还有一个关于定时器的重要概念要向你解释:如果你能够接受定时器有浮点误差,你可以指定容忍度。这个机制允许 iOS 执行一项非常重要的电量优化:它可以在原计划的发射时间和该时间加上容忍时间的区间内发射定时器。实践上,这意味着定时器可以执行 时间合并:它可以推迟一点发射定时器,这样就可能跟某个或者某几个其他的定时器同时发送,从而更多地保持 CPU 空闲,节省电量。

例如,我们给定时器添加半秒的容忍度:

let timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect()

如果你需要保持时间严格准确,那么就不要设置 tolerance 参数。不过请注意,即使没有设置容忍度,Timer 类也只能做到 “尽力而为” —— 系统无法保证定时器精确执行。


我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~

Swift花园微信公众号