58

Timer使用指南

 5 years ago
source link: http://www.cocoachina.com/ios/20180621/23890.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

qeaiiqz.jpg!web

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。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK