Apple 的核心系统库称为 Foundation,它提供了 Data
、 SortDescriptor
、 UserDefaults
等内容。它还提供了 Timer
类,该类主要功能是在一定秒数后运行函数,同时也可以重复运行代码。
如果您想定期运行一些代码,也许是为了制作倒计时器或类似的代码,您应该使用 Timer
和 onReceive()
修饰符。
// 创建计时器需要一个发布者,基本代码如下所示:
// 将整个事物赋值给 `timer` 常量,以便它保持活动状态
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
let timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect()
every
代表每隔多少【秒】发布一次通知
on: .main
代表要求计时器在主线程上运行。
in: .common
代表计时器在公共循环上运行。这样设置可以在用户主动执行某些操作(例如滚动列表)时,程序可以同时处理运行代码
<aside>
💡 使用 .main
作为 runloop 选项非常重要,因为计时器将更新用户界面。至于 .common
模式,它允许计时器与其他常见事件一起运行 - 例如,如果文本位于正在移动的滚动视图中。
</aside>
此外如果您可以容忍计时器有一点偏差,您可以设置容差参数。这使得 iOS 能够执行重要的能量优化,它可以在预定的触发时间,和预定的触发时间加上指定的容差之间的任何点,触发计时器。这意味着系统可以执行计时器合并:它可以将计时器向后推一点,以便它与一个或多个其他计时器同时触发,这意味着它可以让 CPU 更多地空闲并节省电池电量。
如果您需要严格遵守时间,那么省略 tolerance
参数将使计时器尽可能准确,但请注意,即使没有任何容差, Timer
类仍然是“尽力而为”,系统无法保证它会百分百准确执行。
// 例如,这为我们的计时器增加了半秒容差:
let timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect()
autoconnect
方法用于自动连接发布者。当使用 autoconnect
时,发布者会立即开始发布事件,无需显式调用 connect
方法。
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
我们制作的 timer
属性是一个自动连接的发布者,所以我们需要去它的上游发布者找到计时器本身,从那里我们可以连接到计时器发布者,并要求它自行取消。它在代码中的样子如下:
timer.upstream.connect().cancel()
我们还可以让计时器仅触发 n 次,达到固定次数后自动停止。
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 counter == 5 {
timer.upstream.connect().cancel()
} else {
print("The time is now \\(time)")
}
counter += 1
}
}
}
sink
方法用于订阅发布者并处理其发布的值。sink
方法的闭包会在每次发布者发布新值时调用。
let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
let cancellable = timer.sink { time in
print("Timer fired at \\(time)")
}
计时器启动后,它将发送更改通知。我们可以使用名为 onReceive()
的新修饰符,在 SwiftUI 中监视这些更改通知。
onReceive
接受 publish 发布者作为第1个参数,并接受运行的闭包函数作为第2个参数。它将确保每当发布者发送通知时,调用该函数。
// 例如:这将每秒打印一次时间,直到计时器最终停止。闭包的参数 time 是时间
Text("Hello, World!")
.onReceive(timer) { time in
print("The time is now \\(time)")
}
// 例如:创建一个倒计时器来显示标签中的剩余时间:
struct ContentView: View {
@State var timeRemaining = 10
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text("\\(timeRemaining)")
.onReceive(timer) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
}
}
}
}
Timer.scheduledTimer
是 Timer
类的一个类方法,用于创建并调度一个定时器。这个定时器可以在指定的时间间隔后触发一次或反复触发。这个方法的主要功能是让你可以在指定的时间间隔后执行某段代码。当调用 Timer.scheduledTimer
方法时,它会将计时器添加到当前运行循环的默认模式中,并立即启动计时器。
// 从 iOS 10 开始,Timer 提供了一个使用闭包的类方法,这种方法更加简洁和现代:
var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
// 使用闭包创建一个定时器
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
print("Tick")
}
}
deinit {
// 记得在视图控制器销毁时无效化定时器
timer?.invalidate()
}
true
,定时器会在指定的时间间隔重复触发;如果为 false
,定时器只触发一次虽然 Timer.scheduledTimer
是按照固定的间隔进行触发的,但我们可以使用 DispatchQueue.main.asyncAfter
方法来设置延迟。
// 该语句内的命令,会被延迟执行。在使用时要注意哪些命令应该放里面,否则可能会产生数据不一致的Bug
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) {
// print(delay)
}
如果发生了延迟执行导致的数据不一致的情况,还有另外一种方法是:我们可以预先生成好值,只是延迟进行显示
var values: [Int] = []
var currentIndex = 0
func rollDice() {
isRolling = true
let duration = 2.0
let step = 0.1
var time = 0.0
// 预先生成所有值
while time < duration {
values.append(Int.random(in: 1...selectedFaces))
time += step
}
// 使用计时器显示这些值
Timer.scheduledTimer(withTimeInterval: step, repeats: true) { timer in
if currentIndex < values.count {
let progress = Double(currentIndex) / Double(values.count - 1)
let delay = self.easeOutQuad(progress) * step
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
self.currentRoll = self.values[self.currentIndex]
self.currentIndex += 1
if self.currentIndex >= self.values.count {
timer.invalidate()
self.isRolling = false
self.store.add(DiceRoll(faces: self.selectedFaces, result: self.currentRoll, date: Date()))
self.complexSuccess()
}
}
}
}
}
当启动一个计时器,在计时到 10 秒的时候,按下 Home 返回桌面, 等待一段时间再返回应用程序,会发现计时器显示 13 秒左右。这是因为应用程序状态基本有 3 个阶段:.active 活跃
、.inactive 非活跃
、.background 背景
。具体参见: ‣。
因此当我们从应用程序切到桌面,实际上是计时器先进入 .inactive 非活跃
后台状态,然后才进入 .background 背景
暂停运行,最后一直等待应用程序返回。这中间 .inactive 非活跃
的后台状态大概持续 3 秒左右,所以计时器还在运行。
为了解决这个误差,我们可以检测应用程序何时移动到后台或前台,然后适当地暂停并重新启动计时器。
// 通过添加环境属性来判断应用程序当前状态:
@Environment(\\.scenePhase) var scenePhase
@State private var isActive = true
// 使用时:
.onReceive(timer) {
time in
//确保应用程序状态是 活跃,才进行计数;否则退出计数
guard isActive else { return }
if timeRemaining > 0 {
timeRemaining -= 1
}
}
.onChange(of: scenePhase) {
if scenePhase == .active {
isActive = true
} else {
isActive = false
}
}
// 通过手动的精细处理,能够消除 “消失的秒数的误差问题”