iOS计时器

想象一下,您正在开发一个需要定期执行某些操作的应用程序。 这正是Swift使用Timer类的目的。

计时器用于计划应用程序中的操作。 这可以是一次性操作,也可以是重复过程。

在本指南中,您将学习计时器如何在iOS中工作,如何影响UI的响应性,如何在使用计时器时优化电池消耗以及如何将CADisplayLink用于动画。

作为测试站点,我们将使用该应用程序-一个原始任务计划程序。

开始使用


下载源项目。 在Xcode中打开它,查看其结构,进行编译和执行。 您将看到最简单的任务计划程序:



向其中添加一个新任务。 点击+图标,输入任务名称,然后点击确定。

添加的任务带有时间戳。 您刚刚创建的新任务被标记为零秒。 如您所见,该值不会增加。

每个任务都可以标记为已完成。 点击任务。 任务名称将被划掉并标记为已完成。

创建我们的第一个计时器


让我们创建应用程序的主计时器。 Timer类(也称为NSTimer)是一种为特定时刻(单个和周期性)安排动作的便捷方法。

打开TaskListViewController.swift并将此变量添加到TaskListViewController

var timer: Timer? 

然后在其中添加扩展名:

 // MARK: - Timer extension TaskListViewController { } 

并将以下代码粘贴到扩展中:

 @objc func updateTimer() { // 1 guard let visibleRowsIndexPaths = tableView.indexPathsForVisibleRows else { return } for indexPath in visibleRowsIndexPaths { // 2 if let cell = tableView.cellForRow(at: indexPath) as? TaskTableViewCell { cell.updateTime() } } } 

通过这种方法,我们:

  1. 检查任务表中是否有可见行。
  2. 为每个可见的单元格调用updateTime 。 此方法更新单元格中的时间戳(请参阅TaskTableViewCell.swift )。

然后将此代码添加到扩展中:

 func createTimer() { // 1 if timer == nil { // 2 timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true) } } 

我们在这里:

  1. 检查计时器是否包含Timer类的实例。
  2. 如果不是,请创建一个计时器,该计时器每秒调用一次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的开头添加以下变量:

 // 1 var animationTimer: Timer? // 2 var startTime: TimeInterval?, endTime: TimeInterval? // 3 let animationDuration = 3.0 // 4 var height: CGFloat = 0 

这些变量的目的是:

  1. 动画计时器存储。
  2. 存储动画开始和结束的时间。
  3. 动画持续时间。
  4. 动画高度。

现在,在TaskListViewController.swift文件的末尾添加以下TaskListViewController 扩展名

 // MARK: - Animation extension TaskListViewController { func showCongratulationAnimation() { // 1 height = UIScreen.main.bounds.height + balloon.frame.size.height // 2 balloon.center = CGPoint(x: UIScreen.main.bounds.width / 2, y: height + balloon.frame.size.height / 2) balloon.isHidden = false // 3 startTime = Date().timeIntervalSince1970 endTime = animationDuration + startTime! // 4 animationTimer = Timer.scheduledTimer(withTimeInterval: 1 / 60, repeats: true) { timer in // TODO: Animation here } } } 

在这里,我们执行以下操作:

  • 计算动画的高度,获取设备屏幕的高度
  • 将球居中放置在屏幕外并设置其可见性
  • 分配动画的开始和结束时间
  • 我们启动动画计时器并每秒更新动画60次

现在我们需要创建实际的逻辑来更新祝贺动画。 在showCongratulationAnimation()之后添加以下代码:

 func updateAnimation() { // 1 guard let endTime = endTime, let startTime = startTime else { return } // 2 let now = Date().timeIntervalSince1970 // 3 if now >= endTime { animationTimer?.invalidate() balloon.isHidden = true } // 4 let percentage = (now - startTime) * 100 / animationDuration let y = height - ((height + balloon.frame.height / 2) / 100 * CGFloat(percentage)) // 5 balloon.center = CGPoint(x: balloon.center.x + CGFloat.random(in: -0.5...0.5), y: y) } 

我们的工作:

  1. 检查是否已分配endTime和startTime
  2. 节省当前时间
  3. 我们确保最后时间还没有到。 如果已经到达,请更新计时器并隐藏我们的球
  4. 计算球的新y坐标
  5. 相对于先前位置计算出球的水平位置

现在, 使用以下代码替换// 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替换了TimerCADisplayLink是一个计时器视图,与显示器的垂直扫描相关。 这意味着设备的GPU将暂停,直到屏幕可以继续处理GPU命令为止。 这样,我们可以获得流畅的动画。

替换此代码

 var startTime: TimeInterval?, endTime: TimeInterval? 

在这个:

 var startTime: CFTimeInterval?, endTime: CFTimeInterval? 


您用CFTimeInterval替换了TimeInterval ,这对于使用CADisplayLink是必需的。

用以下代码替换showCongratulationAnimation()方法的文本:

 func showCongratulationAnimation() { // 1 height = UIScreen.main.bounds.height + balloon.frame.size.height balloon.center = CGPoint(x: UIScreen.main.bounds.width / 2, y: height + balloon.frame.size.height / 2) balloon.isHidden = false // 2 startTime = CACurrentMediaTime() endTime = animationDuration + startTime! // 3 displayLink = CADisplayLink(target: self, selector: #selector(updateAnimation)) displayLink?.add(to: RunLoop.main, forMode: .common) } 

我们在这里做什么:

  1. 设置动画的高度,球的坐标和可见性-大致与以前相同。
  2. 使用CACurrentMediaTime() (而不是Date())初始化startTime
  3. 我们创建CADisplayLink类的实例,并将其添加到普通模式下的运行循环中。

现在,用以下代码替换updateAnimation()

 // 1 @objc func updateAnimation() { guard let endTime = endTime, let startTime = startTime else { return } // 2 let now = CACurrentMediaTime() if now >= endTime { // 3 displayLink?.isPaused = true displayLink?.invalidate() balloon.isHidden = true } let percentage = (now - startTime) * 100 / animationDuration let y = height - ((height + balloon.frame.height / 2) / 100 * CGFloat(percentage)) balloon.center = CGPoint(x: balloon.center.x + CGFloat.random(in: -0.5...0.5), y: y) } 

  1. objc添加到方法签名(对于CADisplayLink,选择器参数需要这样的签名)。
  2. 将初始化替换为Date()以初始化CoreAnimation的日期。
  3. 用CADisplayLink的暂停替换animationTimer.invalidate()调用并使其无效。 这还将从运行循环中删除CADisplayLink。

启动应用程序!


太好了! 我们成功地使用了更合适的CADisplayLink替换了基于Timer的动画-并使动画更加流畅,而不会发生抖动。

结论


在本指南中,您了解了Timer类在iOS中的工作方式,执行周期是什么以及如何使您的应用程序在界面方面更具响应性,以及如何使用CADisplayLink代替Timer来进行平滑动画处理。

Source: https://habr.com/ru/post/zh-CN450172/


All Articles