Timer 类

Apple 的核心系统库称为 Foundation,它提供了 DataSortDescriptorUserDefaults 等内容。它还提供了 Timer 类,该类主要功能是在一定秒数后运行函数,同时也可以重复运行代码。

如果您想定期运行一些代码,也许是为了制作倒计时器或类似的代码,您应该使用 Timer 和 onReceive() 修饰符。


Timer.publish

// 创建计时器需要一个发布者,基本代码如下所示:
// 将整个事物赋值给 `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

every 代表每隔多少【秒】发布一次通知

参数:on

on: .main 代表要求计时器在主线程上运行。

参数:in

in: .common 代表计时器在公共循环上运行。这样设置可以在用户主动执行某些操作(例如滚动列表)时,程序可以同时处理运行代码

<aside> 💡 使用 .main 作为 runloop 选项非常重要,因为计时器将更新用户界面。至于 .common 模式,它允许计时器与其他常见事件一起运行 - 例如,如果文本位于正在移动的滚动视图中。

</aside>

参数:tolerance 容差

此外如果您可以容忍计时器有一点偏差,您可以设置容差参数。这使得 iOS 能够执行重要的能量优化,它可以在预定的触发时间,和预定的触发时间加上指定的容差之间的任何点,触发计时器。这意味着系统可以执行计时器合并:它可以将计时器向后推一点,以便它与一个或多个其他计时器同时触发,这意味着它可以让 CPU 更多地空闲并节省电池电量。

如果您需要严格遵守时间,那么省略 tolerance 参数将使计时器尽可能准确,但请注意,即使没有任何容差, Timer 类仍然是“尽力而为”,系统无法保证它会百分百准确执行。

// 例如,这为我们的计时器增加了半秒容差:
let timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect()

方法:autoconnect 自动开始

autoconnect 方法用于自动连接发布者。当使用 autoconnect 时,发布者会立即开始发布事件,无需显式调用 connect 方法。

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

方法:cancel 停止

我们制作的 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 方法用于订阅发布者并处理其发布的值。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(Timer)

计时器启动后,它将发送更改通知。我们可以使用名为 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.scheduledTimerTimer 类的一个类方法,用于创建并调度一个定时器。这个定时器可以在指定的时间间隔后触发一次或反复触发。这个方法的主要功能是让你可以在指定的时间间隔后执行某段代码。当调用 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()
}

主要参数

在计时之间设置延时

虽然 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
		}
}

// 通过手动的精细处理,能够消除 “消失的秒数的误差问题”