转载

Timer使用指南

Timer使用指南

Swift的Timer类(前身是NSTimer)是一种灵活规划未来预定事件的方法,可以仅触发一次或不断循环。在这篇指南中我会提供多种使用它的方式,并带有一些常见问题的解决办法。

注意:我要首先声明,使用timers会有很大的电力消耗。我们会想办法减少它,但任何类型的timers要想触发,都要从静止状态下唤醒系统,并会有相应的消耗。

创建一个循环timer

从最基础的开始,创建并启用一个循环timer来调用一个方法:

let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: true)

这里我们为了测试用了fireTimer()方法:

@objc func fireTimer() {
    print("Timer fired!")
}

尽管我们要求timer每隔1.0秒触发一次,iOS会让timer稍微有点宽容度——可能你的timer很难精确的间隔1.0秒触发。

另一个创建循环timer的常用方法是使用闭包:

let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
    print("Timer fired!")
}

这些初始化都可以创建timer,不需要把它存在某个属性中,但那样做会比较好,能够方便晚些终止这个timer。因为闭包方法每次代码运行时都要通过timer,你也可以从这方面终止它。

创建一个非循环timer

如果你想代码只运行一次,就把repeats: true改成repeats: false

let timer1 = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: false)
 
let timer2 = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { timer in
    print("Timer fired!")
}

其他代码不变

尽管这种方法听上去很完美,我个人还是推荐用GCD来实现

DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    print("Timer fired!")
}

结束一个timer

你可以调用invalidate()方法来销毁已存在的timer

例如下面代码创建每秒打印“Timer fired!”1次,共打印3次的timer,之后终止它。

var runCount = 0
 
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
    print("Timer fired!")
    runCount += 1
 
    if runCount == 3 {
        timer.invalidate()
    }
}

如果通过一个方法结束一个timer,首先需要声明一个timer和一个runCount属性

var timer: Timer?
var runCount = 0

之后规划好timer

timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: true)

最后,填写fireTimer()方法

@objc func fireTimer() {
    print("Timer fired!")
    runCount += 1
 
    if runCount == 3 {
        timer?.invalidate()
    }
}

另一种方法是,让fireTimer()接收timer作为其参数,这样就不需要使用timer属性。需要这样重写fireTimer()

@objc func fireTimer(timer: Timer) {
    print("Timer fired!")
    runCount += 1
 
    if runCount == 3 {
        timer.invalidate()
    }
}

附加context

当你创建timer来执行一个方法时,你可以附加一些context,用于存储额外的timer触发条件信息。它是一个字典,可以存任意量的数据——比如触发timer的事件,用户在做些什么,哪个table view被选中等等

比如我们可以让这个字典包含有一个用户名:

let context = ["user": "@twostraws"]
Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: context, repeats: true)

我们之后可以通过查看timer 参数的userInfo属性来读取fireTimer()

@objc func fireTimer(timer: Timer) {
    guard let context = timer.userInfo as? [String: String] else { return }
    let user = context["user", default: "Anonymous"]
 
    print("Timer fired by /(user)!")
    runCount += 1
 
    if runCount == 3 {
        timer.invalidate()
    }
}

添加一些时间宽容度(tolerance)

给你的timer添加一些时间宽容度可以降低它的电力消耗。它允许你给系统留一些timer执行时间的冗余。“我希望1秒钟运行一次,但是晚个200毫秒我也不介意”。这允许系统协同运行多个timer,把多个timer事件合并到一起,节省电池寿命。

当你指定了时间宽容度,就意味着系统可以在原有时间附加该宽容度内的任意时刻触发timer。例如,如果你要timer 1秒后运行,并有0.5秒的时间宽容度,实际就可能是1秒,1.5秒或1.3秒等。

下例中创建了一个1秒运行一次的timer,并有0.2秒的时间宽容度:

let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: true)
timer.tolerance = 0.2

默认的时间宽容度是0,但是系统会自动添加一个很小的宽容度

如果一个重复性timer由于设定的时间宽容度推迟了一小会执行,这并不意味着后续的执行都会晚一会。iOS不允许timer总体上的漂移,也就是说下一次触发会快一些。

举例的话,如果一个timer每1秒运行一次,并有0.5秒的时间宽容度,那么实际可能是这样:

  • 1.0秒后timer触发

  • 2.4秒后timer再次触发,晚了0.4秒,但是在时间宽容度内

  • 3.1秒后timer第三次触发,和上一次仅差0.7秒,但每次触发的时间是按原始时间算的。

  • 等等…

与runloops协同使用

在app中实际使用中,人们经常会遇到timer并没有触发的情况。比如用户用手指触摸屏幕,滚动一个table view的时候,即使设定好条件timer也不会触发。

这是由于我们默认把timer创建为defaultRunLoopMode,这是我们app的主线程。所以当用户与UI正在互动时会暂停,当用户停下后才再次触发。

最简单的解决办法是在创建timer时不直接规划它,而是手动把它添加到一个runloop中。本例中,我们选用了.commonModes:即使UI正在使用,它也允许timer触发。

let context = ["user": "@twostraws"]
let timer = Timer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: context, repeats: true)
RunLoop.current.add(timer, forMode: .commonModes)

把timer与屏幕刷新同步

一些人,尤其是游戏开发者,会尝试在每帧被绘制之前让timer完成一些工作。

但这是错误的:timer并不具备这么高的精确度,人们也无法知道上一帧被绘制后过去了多少时间。你也许会设置每秒运行60或120次代码,但实际上在你的timer触发之前可能半数都被跳过了。

所以如果你想要一些代码在屏幕刷新后立即运行,你要使用CADisplayLink。下面是一些关于CADisplayLink的代码段

let displayLink = CADisplayLink(target: self, selector: #selector(fireTimer))
displayLink.add(to: .current, forMode: .defaultRunLoopMode)

别忘了,如果你想要DisplayLink方法在UI被使用时也能触发,请指定.commonModes,而不是用.defaultRunLoopMode。

正文到此结束
Loading...