想象一下,您正在开发一个需要定期执行某些操作的应用程序。 这正是Swift使用
Timer类的目的。
计时器用于计划应用程序中的操作。 这可以是一次性操作,也可以是重复过程。
在本指南中,您将学习计时器如何在iOS中工作,如何影响UI的响应性,如何在使用计时器时优化电池消耗以及如何将
CADisplayLink用于动画。
作为测试站点,我们将使用该应用程序-一个原始任务计划程序。
开始使用
下载
源项目。 在Xcode中打开它,查看其结构,进行编译和执行。 您将看到最简单的任务计划程序:

向其中添加一个新任务。 点击+图标,输入任务名称,然后点击确定。
添加的任务带有时间戳。 您刚刚创建的新任务被标记为零秒。 如您所见,该值不会增加。
每个任务都可以标记为已完成。 点击任务。 任务名称将被划掉并标记为已完成。
创建我们的第一个计时器
让我们创建应用程序的主计时器。
Timer类(也称为
NSTimer)是一种为特定时刻(单个和周期性)安排动作的便捷方法。
打开
TaskListViewController.swift并将此变量添加到
TaskListViewController :
var timer: Timer?
然后在其中添加扩展名:
并将以下代码粘贴到扩展中:
@objc func updateTimer() {
通过这种方法,我们:
- 检查任务表中是否有可见行。
- 为每个可见的单元格调用updateTime 。 此方法更新单元格中的时间戳(请参阅TaskTableViewCell.swift )。
然后将此代码添加到扩展中:
func createTimer() {
我们在这里:
- 检查计时器是否包含Timer类的实例。
- 如果不是,请创建一个计时器,该计时器每秒调用一次updateTimer() 。
然后,我们需要在用户添加第一个任务后立即创建一个计时器。 在
presentAlertController(_ :)方法的开始处添加
createTimer() 。
启动应用程序并创建几个新任务。 您将看到每个任务的时间戳每秒钟更改一次。

增加计时器公差
计时器数量的增加会导致UI响应性变差并消耗更多电池。 每个计时器都会尝试在分配给它的时间准确执行,因为默认情况下,其容差为零。
增加计时器公差是减少能耗的简便方法。 这样,系统就可以在分配的时间和分配的时间
加容差时间之间执行计时器操作,但绝不能在分配的间隔之前执行。
对于仅运行一次的计时器,公差值将被忽略。
在
createTimer()方法中,在分配计时器之后,立即添加以下行:
timer?.tolerance = 0.1
启动应用程序。 在这种特殊情况下,效果不会很明显(我们只有一个计时器),但是,在实际使用多个计时器的情况下,您的用户将获得响应速度更快的界面,并且应用程序将更加节能。

后台计时器
有趣的是,当应用程序进入后台时,计时器会发生什么? 为了解决这个问题,请在
updateTimer()方法的开头添加以下代码:
if let fireDateDescription = timer?.fireDate.description { print(fireDateDescription) }
这将使我们能够在控制台中跟踪计时器事件。
运行应用程序,添加任务。 现在,按设备上的“主页”按钮,然后返回到我们的应用程序。
在控制台中,您将看到以下内容:

如您所见,当应用程序进入后台时,iOS会暂停所有正在运行的应用程序计时器。 当应用程序变为活动状态时,iOS将恢复计时器。
了解运行循环
运行循环是一个事件循环,可以安排工作并处理传入的事件。 该周期使线程在运行时保持繁忙,并在没有工作时将其置于“睡眠”状态。
每次启动应用程序时,系统都会创建应用程序的主线程,每个线程都有一个为其自动创建的执行循环。
但是,为什么现在所有这些信息对您都很重要? 现在,每个计时器都在主线程中启动,并加入执行循环。 您可能知道主线程参与了呈现用户界面,处理触摸等操作。 如果主线程忙于处理某些事情,则应用程序的接口可能会变得“无响应”(挂起)。
您是否注意到拖动表格视图时未更新单元格中的时间戳?

您可以通过告诉运行周期以其他模式启动计时器来解决此问题。
了解运行周期模式
执行周期模式是一组输入源,例如触摸屏幕或单击鼠标(可设置为监视)和一组接收通知的“观察者”。
iOS中有三种运行时模式:
default :处理非NSConnectionObjects的输入源。
common :正在处理一组输入周期,您可以为其定义一组输入源,计时器,“观察者”。
tracking :正在处理应用程序UI。
对于我们的应用,最合适的模式是
常见的 。 要使用它,请将以下内容替换
createTimer()方法
的内容:
if timer == nil { let timer = Timer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true) RunLoop.current.add(timer, forMode: .common) timer.tolerance = 0.1 self.timer = timer }
与之前的代码的主要区别在于,在将计时器分配给TaskListViewController之前,我们将此计时器添加到
普通模式下的运行循环中。
编译并运行该应用程序。

现在,即使表格已滚动,单元的时间戳也会更新。
添加动画以完成所有任务
现在,我们为用户添加了一个祝贺性的动画来完成所有任务-球将从屏幕底部上升到顶部。
在TaskListViewController的开头添加以下变量:
这些变量的目的是:
- 动画计时器存储。
- 存储动画开始和结束的时间。
- 动画持续时间。
- 动画高度。
现在,在
TaskListViewController.swift文件的末尾添加以下TaskListViewController
扩展名 :
在这里,我们执行以下操作:
- 计算动画的高度,获取设备屏幕的高度
- 将球居中放置在屏幕外并设置其可见性
- 分配动画的开始和结束时间
- 我们启动动画计时器并每秒更新动画60次
现在我们需要创建实际的逻辑来更新祝贺动画。 在
showCongratulationAnimation()之后添加以下代码:
func updateAnimation() {
我们的工作:
- 检查是否已分配endTime和startTime
- 节省当前时间
- 我们确保最后时间还没有到。 如果已经到达,请更新计时器并隐藏我们的球
- 计算球的新y坐标
- 相对于先前位置计算出球的水平位置
现在,
使用以下代码替换
// TODO: showCongratulationAnimation()中的 动画 :
self.updateAnimation()
现在,只要发生计时器事件,就会调用
updateAnimation() 。
万岁,我们刚刚创建了一个动画。 但是,当应用程序启动时,没有新的事情发生……
显示动画
您可能已经猜到了,没有什么可以“推出”我们的新动画了。 为此,我们需要另一种方法。 将此代码添加到TaskListViewController动画
扩展中 :
func showCongratulationsIfNeeded() { if taskList.filter({ !$0.completed }).count == 0 { showCongratulationAnimation() } }
每当用户将任务标记为完成时,我们都会调用此方法,他会检查所有任务是否已完成。 如果是这样,它将调用
showCongratulationAnimation() 。
最后,在
tableView(_:didSelectRowAt :)的末尾添加对此方法的调用:
showCongratulationsIfNeeded()
启动应用程序,创建几个任务,将其标记为已完成-您将看到我们的动画!

我们停止计时器
如果您查看控制台,则会看到,尽管用户将所有任务标记为已完成,但计时器仍将继续工作。 这是完全没有意义的,因此在不需要计时器时停止计时器是有意义的。
首先,创建一个新的方法来停止计时器:
func cancelTimer() { timer?.invalidate() timer = nil }
这将更新计时器并将其重置为nil,以便我们以后可以正确地再次创建它。
invalidate()是从运行循环中删除Timer的唯一方法。 运行循环将在调用
invalidate()之后立即删除强定时器引用,或者稍后再删除。
现在,按如下所示替换showCongratulationsIfNeeded()方法:
func showCongratulationsIfNeeded() { if taskList.filter({ !$0.completed }).count == 0 { cancelTimer() showCongratulationAnimation() } else { createTimer() } }
现在,如果用户完成了所有任务,则应用程序将首先重置计时器,然后显示动画,否则,如果尚未存在,它将尝试创建一个新计时器。
启动应用程序。

现在,计时器停止并重新启动。
CADisplayLink使动画流畅
计时器不是控制动画的理想选择。 您可能已经注意到跳过动画的几帧,尤其是在模拟器中运行应用程序时。
我们将计时器设置为60Hz。 因此,计时器每16毫秒更新一次动画。 仔细考虑一下情况:

使用
计时器时,我们不知道操作开始的确切时间。 这可以发生在帧的开始或结尾。 假设计时器在每一帧的中间运行(图片中的蓝点)。 我们唯一可以确定的是,通话将每16毫秒一次。
现在我们只有8毫秒的时间来执行动画,这对于我们的动画来说可能还不够。 让我们看一下图中的第二帧。 第二帧无法在指定时间内完成,因此应用程序将重置动画的第二帧。
CADisplayLink将帮助我们
CADisplayLink每帧调用一次,并尝试尽可能多地同步真实动画帧。 现在您将拥有所有16毫秒的可用时间,iOS将不会丢掉一个帧。
要使用
CADisplayLink ,需要用新类型替换
animationTimer 。
替换此代码
var animationTimer: Timer?
在这个:
var displayLink: CADisplayLink?
您已用
CADisplayLink替换了
Timer 。
CADisplayLink是一个计时器视图,与显示器的垂直扫描相关。 这意味着设备的GPU将暂停,直到屏幕可以继续处理GPU命令为止。 这样,我们可以获得流畅的动画。
替换此代码
var startTime: TimeInterval?, endTime: TimeInterval?
在这个:
var startTime: CFTimeInterval?, endTime: CFTimeInterval?
您用
CFTimeInterval替换了
TimeInterval ,这对于使用CADisplayLink是必需的。
用以下代码替换
showCongratulationAnimation()方法的文本:
func showCongratulationAnimation() {
我们在这里做什么:
- 设置动画的高度,球的坐标和可见性-大致与以前相同。
- 使用CACurrentMediaTime() (而不是Date())初始化startTime 。
- 我们创建CADisplayLink类的实例,并将其添加到普通模式下的运行循环中。
现在,用以下代码替换
updateAnimation() :
- 将objc添加到方法签名(对于CADisplayLink,选择器参数需要这样的签名)。
- 将初始化替换为Date()以初始化CoreAnimation的日期。
- 用CADisplayLink的暂停替换animationTimer.invalidate()调用并使其无效。 这还将从运行循环中删除CADisplayLink。
启动应用程序!

太好了! 我们成功地使用了更合适的
CADisplayLink替换了基于
Timer的动画-并使动画更加流畅,而不会发生抖动。
结论
在本指南中,您了解了
Timer类在iOS中的工作方式,执行周期是什么以及如何使您的应用程序在界面方面更具响应性,以及如何使用
CADisplayLink代替Timer来进行平滑动画处理。