转载

iOS 10 by Tutorials 笔记(十四)

Chapter 14: Other iOS 10 Topics

iOS 10 还有许多新特性,集中放到最后一章来说下吧,主要分三个主题

  • Data Source Prefetching 当单元格显示在屏幕前,预先对要显示的数据进行处理以提升应用的性能
  • UIPreviewInteraction 新的协议允许用户通过 3D Touch 进行新的交互
  • Taptic Feedback 随着 iPhone 7 和 7 Plus 硬件的更新,iOS 10 也为新的线性马达也提供了与之匹配的 API

本章我们来完成一个 EmojiRater 应用,它是一个 collection view 来显示各种 emoji 表情 iOS 10 by Tutorials 笔记(十四)

我们将利用新的 API 对 collection view 预取数据过程提速,然后就能在预览交互部分对 emojis 表情评分。最后当用户滚动到顶部时能感到视觉反馈。

Data source prefetching

在本节,你可以为 EmojiRater 添加数据源预取功能。

数据源预取是指:在需要显示之前准备其内容数据的机制。可以想象下应用的 cell 包含需要从网络下载的图片,我们可以通过提前预取(新开个下载线程)操作来减少延迟

下面有请新的 data source protocol 登场 UICollectionViewDataSourcePrefetching ,由此协议负责数据的预取操作,它只定义了两个方法:

  • collectionView(_:prefetchItemsAt:) 系统根据当前的滚动方向和速度,对外传递了要显示的 cells 索引数组,而且这些数组元素会按照紧急程度进行排序(即视图什么需要显示这些数据)。通常我们会根据这些索引单独写一个数据读取操作。
  • collectionView(_:cancelPrefetchingForItemsAt:) 这是个可选的方法,当你需要取消预取操作时会被触发,同样它会接受一个需要取消的索引数组。(通常发生在用户改变滚动方向时)

对于那些很大、需要消耗时间处理的数据源,实现此协议可以极大地改善用户体验,当然内部并没什么复杂的原理,它只是靠猜测用户下一刻的动作来决定预取的动作。如果用户滚动地非常快,或者资源受限,预取请求将变慢或者停止。

collection view 会在用户滚动时调用此方法,提供下一刻将要显示的单元索引。你可以根据这些索引来提前进行数据的准备工作。但是数据的提取过程必须是异步的,并且将结果交给数据源 collectionView(_:cellForItemAt:) 方法 如果你使用的是 TableView,那也没关系,可以去看看 UITableViewDataSourcePrefetching 协议

Implementing UICollectionViewDataSourcePrefetching

我们先将注意力集中在 EmojiCollectionViewController.swift 这个文件上,它其实是 Collection ViewController 用来显示 EmojiCollectionViewCell 对象的,这些 cells 当前只显示一个 emoji 表情。

我们在 collectionView(_:willDisplay:forItemAt:) 方法中配置这些 cell,数据源来自于 loadingOperations 字典,它是由索引和 DataLoadOperation (载入数据操作)组成的键值对。 DataLoadOperation 属于 Operation 的子类,主要负责载入 emoji 表情内容。

collectionView(_:willDisplay:forItemAt:) 触发时,DataLoadOperation 根据其提供的索引值(indexPath)将相关对象拉入载入队列。

运行一下,向下滚动,能看到很多小菊花转啊转的载入数据~ iOS 10 by Tutorials 笔记(十四)

其实这里只是通过随机睡眠几秒的方法,模拟了从网络加载数据等耗时的操作。不过这种体验还是太糟糕,我们来改进一下。

打开 EmojiCollectionViewController.swift,在底部添加:

extension EmojiCollectionViewController:  
UICollectionViewDataSourcePrefetching {  
  func collectionView(_ collectionView: UICollectionView,
                      prefetchItemsAt indexPaths: [IndexPath]) {
    print("Prefetch: /(indexPaths)")
  }
}

这里实现了 UICollectionViewDataSourcePrefetching 协议的 collectionView(_:prefetchItemsAt:) 方法,当 collection view 预感将要载入某些 cells 时,就会将对应的索引传回来,在此我们只打印了这些索引。

回到 viewDidLoad() 在顶部指定协议的实现者

collectionView?.prefetchDataSource = self

先运行一下什么操作都不做,观察下终端的输出

Prefetch: [[0, 8], [0, 9], [0, 10], [0, 11], [0, 12], [0, 13]]

collection view 非常聪明,它已经知道此时 view 的位置停留在顶部,因此只可能向下滑动,所以期望预取的结果就是从第九条 cell 开始(iPhone 6s 一屏显示 8 条)。随便滑动几下并观察终端输出,你会发现滚动的速度越快就需要请求预取更多的 cells,而且滚动的方向和所处的位置都会决定 cell 如何预取。

因为 cell 还未呈现在屏幕上,所以还不能设置它的界面部分。但可以提前准备显示内容,具体过程就是在后台队列中执行 DataLoadOperation 操作,下面来实现下,还是在 collectionView(_:prefetchItemsAt:) 方法中(替换之前的打印方法):

// 1 indexPaths 是按优先级排序的数组,越紧急的顺序越靠前
for indexPath in indexPaths {  
  // 2 如果已经提供索引对应的 operation 已经存在了就继续
  if let _ = loadingOperations[indexPath] {
    continue
  }
  // 3 根据索引创建 DataLoadOperation 对象,并放到队列和字典中
  if let dataLoader = dataStore.loadEmojiRating(at: indexPath.item) {
    loadingQueue.addOperation(dataLoader)
    loadingOperations[indexPath] = dataLoader
  }
}

运行,等待一下给应用留点时间载入数据,再缓慢的滚动,这次你会发现 cells 的载入速度有了显著提高: iOS 10 by Tutorials 笔记(十四)

这套工作机制在用户规律地滚动 collection view 时很管用,但如果用户突然改变滚动方向,很可能预取的那些 cell 就不需要了,因此我们要能取消不必要的预取请求, UICollectionViewDataSourcePrefetching 协议也提供了取消方法:

func collectionView(_ collectionView:  
  UICollectionView, cancelPrefetchingForItemsAt indexPaths:
  [IndexPath]) {
  for indexPath in indexPaths {
    if let dataLoader = loadingOperations[indexPath] {
      dataLoader.cancel()
      loadingOperations.removeValue(forKey: indexPath)
    }
  } 
}

取消判断也很简单,如果一个载入操作(loading operation)已经存在,那么我们就取消载入操作,然后从 loadingOperations 字典中移除

再次运行,随便滚动几下,你不会发现任何异样,但不必要的载入操作已经被取消了。

不过并不 100% 保证所有操作都被取消,毕竟这涉及到苹果的私有算法。

UIPreviewInteraction

iOS 9 带来了全新的 3D Touch 交互操作(Peek and Pop),你可以使用 Peek and Pop 来控制显示内容和快速执行一些动作。但你不能自定义过渡动画和用户的交互方式

iOS 10 推出了全新的 UIPreviewInteraction API,它让我们可以创建自定义的预览交互操作(类似于 Peek 和 Pop 的概念),即通过不断对屏幕施压,然后触发 3D Touch 的两种界面状态,同时也会伴随着独特的触觉反馈(振动马达)

不同于 Peek 和 Pop,这些交互限于导航。

  • Preview 是第一种状态,这是你施展动画的地方
  • Commit 是第二种状态,这是你施展交互的地方

下面的例子展示了这几种状态 iOS 10 by Tutorials 笔记(十四)

在 Preview 状态渐渐隐去背景,将焦点集中在选择的 cell 上,然后把投票的控件放到 emoji 上。在 commit 状态,上下移动你的手指按压进行评价。松开手指评分会出现在 cell 上。

为了实现该功能,我们需要 UIPreviewInteractionDelegate ,它负责监听整个交互过程,并接收相关进度消息。下面是几个关键节点:

  • previewInteractionShouldBegin(_:) 当 3D Touch 开始一个 preview 进程时,在这里我们可以进行动画对象的设置
  • previewInteraction(_:didUpdatePreviewTransition:ended:) 执行 preview 过程,系统给我们传达两个参数:1.执行进程 2.完成情况
  • previewInteractionDidCancel(_:) 当 preview 进程被取消时被调用
  • previewInteraction(_:didUpdateCommitTransition:ended:) 执行 commit 过程,与执行 preview 过程类似,同样接收两个参数。

本节 Demo 需要在支持 3D Touch 的机器上运行,即至少是 iPhone 6s 级别的手机。

Exploring UIPreviewInteractionDelegate

我们现在开始来实现 UIPreviewInteractionDelegate 协议,打开 EmojiCollectionViewController.swift 文件,添加下面的属性

var previewInteraction: UIPreviewInteraction?

UIPreviewInteraction对象带一个 view 属性(能配置)可以进行 3D Touch 交互,稍后来创建它。

找到 viewDidLoad() 中创建 ratingOverlayView 的地方,RatingOverlayView 可以看做是一个与屏幕大小相等的蒙版,稍后要由它来负责一系列的动画和交互效果,即创建一个背景模糊动画效果然后焦聚在某个 cell 上,然后在此之上覆盖一个评分控件。

创建完 ratingOverlayView 后,我们来创建一个用 collectionView 做交互区域的 UIPreviewInteraction 对象,并且它的 delegate 方法交给 self 来实现。

if let collectionView = collectionView {  
  previewInteraction = UIPreviewInteraction(view: collectionView)
  previewInteraction?.delegate = self
}

实现所谓的 UIPreviewInteractionDelegate,当前只是进行了打印操作

extension EmojiCollectionViewController: UIPreviewInteractionDelegate {  
  func previewInteraction(_ previewInteraction:
    UIPreviewInteraction, didUpdatePreviewTransition
    transitionProgress: CGFloat, ended: Bool) {
    print("Preview: /(transitionProgress), ended: /(ended)")
}
  func previewInteractionDidCancel(_ previewInteraction:
    UIPreviewInteraction) {
    print("Canceled")
  }
}

在真机上运行一下,对某个 cell 进行 3D Touch 按压操作,感受马达的振动反馈,并观察终端输出:

Preview: 0.0, ended: false  
Preview: 0.0970873786407767, ended: false  
Preview: 0.184466019417476, ended: false  
Preview: 0.271844660194175, ended: false  
Preview: 0.330097087378641, ended: false  
Preview: 0.378640776699029, ended: false  
Preview: 0.466019417475728, ended: false  
Preview: 0.543689320388349, ended: false  
Preview: 0.631067961165048, ended: false  
Preview: 0.747572815533981, ended: false  
Preview: 1.0, ended: true  
Canceled

UIPreviewInteractionDelegate 还有两个可选的方法

func previewInteractionShouldBegin(_ previewInteraction:  
  UIPreviewInteraction) -> Bool {
  print("Preview should begin")
  return true
}

func previewInteraction(_ previewInteraction:  
  UIPreviewInteraction, didUpdateCommitTransition
  transitionProgress: CGFloat, ended: Bool) {
  print("Commit: /(transitionProgress), ended: /(ended)")
}

前者在 preview 开始时触发并打印日志,这里返回 true 是让 preview 进程开始;后者方法类似于监听 preview 过程

再次运行执行 3D Touch 操作,观察终端输出的完整的生命周期日志

Preview should begin  
Preview: 0.0, ended: false  
Preview: 0.567567567567568, ended: false  
Preview: 1.0, ended: true  
Commit: 0.0, ended: false  
Commit: 0.252564102564103, ended: false  
Commit: 0.340009067814572, ended: false  
Commit: 0.487818348221377, ended: false  
Commit: 0.541819501609486, ended: false  
Commit: 0.703165992497785, ended: false  
Commit: 0.902372307312938, ended: false  
Commit: 1.0, ended: true

Implementing a custom interaction

理清了这些 delegate 调用流程,现在就能更好地设置自定义交互了,首先来构建一个 helper 方法,即传入一个 UIPreviewInteraction 参数,返回一个 Cell;其实就是当 3D Touch 事件发生时找到所按压的 cell

func cellFor(previewInteraction: UIPreviewInteraction)  
  -> UICollectionViewCell? {
  if let indexPath = collectionView?
    .indexPathForItem(at: previewInteraction
      .location(in: collectionView!)),
    let cell = collectionView?.cellForItem(at: indexPath) {
    return cell
  } else {
    return nil
  } 
}

我们通过 UIPreviewInteraction 对象的 location(in:) 方法找到了按压位置,进而找出对应的 cell

现在该来完善之前仅仅打印日志的 deleage 交互方法了,找到 previewInteractionShouldBegin(_:) 更新为如下代码:

// 1 先保证找出所按压的 cell
guard let cell = cellFor(previewInteraction:  
  previewInteraction) else {
return false  
}
// 2 开始 3D Touch 交互动画,并禁止 scroll 滚动
ratingOverlayView?.beginPreview(forView: cell)  
collectionView?.isScrollEnabled = false  
return true

第二步,我们在 ratingOverlayView 上手动实现了一个类似 3D Touch 背景模糊并焦聚的动画,通过 beginPreview 方法来触发

接下来在 previewInteraction(_:didUpdatePreviewTransition:ended:) 方法中,我们让手动实现的 ratingOverlayView 动画进程与 preview 进程同步(基于系统提供的 transitionProgress 来控制动画)

func previewInteraction(_ previewInteraction: UIPreviewInteraction,  
  didUpdatePreviewTransition transitionProgress: CGFloat, ended: Bool) {
  ratingOverlayView?.updateAppearance(forPreviewProgress: transitionProgress)
}

运行一下,缓慢地按压某个 cell,背景开始变得模糊,评分控件最后也加上去了 iOS 10 by Tutorials 笔记(十四)

但是手指挪开,整个动画就冻结了,这是因为交互完成后,我们并没有清理动画 iOS 10 by Tutorials 笔记(十四)

我们在 previewInteractionDidCancel(_:) 中结束交互并恢复滚动

ratingOverlayView?.endInteraction()  
collectionView?.isScrollEnabled = true

在 endInteraction() 方法的实现中,我们反转动画到开始前的状态。再次运行,按压---移开手指,这次一切正常了 iOS 10 by Tutorials 笔记(十四)

当我们继续重压,触发第二级感应反馈时来实现评分操作。更新 previewInteraction(_:didUpdateCommitTransition:ended:) 方法

let hitPoint = previewInteraction.location(in: ratingOverlayView!)  
if ended {  
  // TODO commit new rating
} else {
  ratingOverlayView?.updateAppearance(forCommitProgress:
    transitionProgress, touchLocation: hitPoint)
}

updateAppearance(forCommitProgress:touchLocation:) 方法根据按压的位置和进程,来选择 评分控件 触发高亮动画进程。

再次运行,持续按压通过一级反馈后,进入评分环境,上下移动手指选择『点赞』或『踩踩』 iOS 10 by Tutorials 笔记(十四)

当点评操作完成后,即 ended 参数为 true,我们就要将所选的评分手势图片覆盖到 emoji 表情上,这里我们创建一个独立的 helper 方法来完成

func commitInteraction(_ previewInteraction:  
  UIPreviewInteraction, hitPoint: CGPoint) {
// 1 根据点击区域判断所选评分手势图片
  let updatedRating = ratingOverlayView?
    .completeCommit(at: hitPoint)
// 2 找出对应的 cell 和 cell 上的 emoji
  guard let cell = cellFor(previewInteraction:
    previewInteraction) as? EmojiCollectionViewCell,
    let oldEmojiRating = cell.emojiRating else {
      return
}
// 3 创建一个新的 EmojiRating 对象,传入旧的 emoji 和评分
//   最后更新 cell 并开启滚动
  let newEmojiRating = EmojiRating(emoji: oldEmojiRating.emoji,
                                   rating: updatedRating!)
  dataStore.update(emojiRating: newEmojiRating)
  cell.updateAppearanceFor(newEmojiRating)
  collectionView?.isScrollEnabled = true
}

回到 previewInteraction(_:didUpdateCommitTransition:ended:) 方法,替换 //TODO

commitInteraction(previewInteraction, hitPoint: hitPoint)

最后运行,选择某个 cell 缓慢按压,当你感受到一级振动反馈后,上下移动手指选择评分手势图片继续重压,直到感受到二级振动反馈后确认结果。 iOS 10 by Tutorials 笔记(十四)

Haptic feedback

本节要介绍的 Haptic 其实是指 iPhone 7 和 iPhone 7 Plus 新的线性震动马达所带来的多级触觉反馈,苹果 iOS 10 的 API 允许开发者在他们的应用程序中使用 Taptic 引擎,新的 Taptic 带来了多级触觉反馈,这意味着开发者可以对应地采取一些行动。

UIFeedbackGenerator 类是所有反馈的抽象类,它有三个子类

  • UIImpactFeedbackGenerator 主要指示两个视图元素之间的影响,比如碰撞发生时提供振动反馈等
  • UINotificationFeedbackGenerator 用来指示任务的完成,为其提供振动反馈
  • UISelectionFeedbackGenerator 用来指示选中某个选项时,提供振动反馈

使用这些发生器也非常简单:

let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)  
feedbackGenerator.impactOccurred()

这里创建了一个 heavy 风格的 UIImpactFeedbackGenerator 发生器,触发它也很简单,调用 impactOccurred() 方法即可

Implementing UIFeedbackGenerator

我们可以让 EmojiRater 应用的 collectionView 滚动到顶部时触发 UIImpactFeedbackGenerator 振动反馈。打开 EmojiCollectionViewController.swift 在 UICollectionViewDelegate 标记下添加一个方法

override func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {  
  let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
  feedbackGenerator.impactOccurred()
}

表示滚动到顶部时触发一个振动反馈,运行一下,先滚动到下面再单击顶部区域让 view 回滚到顶部,感受下马达振动的感觉。 iOS 10 by Tutorials 笔记(十四)

UIImpactFeedbackGenerator 适用于用户 UI 交互,你可以试一下另外两种类型的反馈,分别替换 scrollViewDidScrollToTop(_:) 方法为通知类型的反馈

let feedbackGenerator = UINotificationFeedbackGenerator()  
feedbackGenerator.notificationOccurred(.success)

以及选择类型的反馈

let feedbackGenerator = UISelectionFeedbackGenerator()  
feedbackGenerator.selectionChanged()

用心去感受不同的类型的振动反馈吧,当然你得要有个 iPhone 7 或 iPhone 7 plus 才行,手动滑稽 ��

终于写完啦,全书完,继续看前端去了~

-EOF-
原文  https://chengwey.com/ios-10-by-tutorials-bi-ji-shi-si/
正文到此结束
Loading...