本文是 WWDC 2018 Session 225 读后感,其视频及配套 PDF文稿 地址以下 A Tour Of UICollectionView。swift
这篇文章难度不大,由易到难,逐层深刻,是一篇很好的 Session。全文总计约2500字,通读全文花费时间大约15分钟。数组
看完这篇 Session,给个人直观感觉是这篇名为 A Tour Of UICollectionView 的文章,是围绕着一个 CollectionView 的案例,对自定义布局以及其性能优化、数据操做、动画作的一次探讨。虽然没有新增的 API 和特性,可是实际意义蛮大。缓存
咱们也按照 Session 的思路,将本文主要分为三个模块:安全
CollectionView 想必各位已经不陌生了,在咱们的平常开发中,它的身影随处可见。若是还有小伙伴对它不熟悉,能够看看以前的 Session :性能优化
若是咱们想搭建一个以下图的 App ,须要涉及到三点:布局、刷新、动画,咱们今天的话题也是围绕着这三点展开。闭包
CollectionView 的核心概念有三点:布局(Layout)、数据源(Data Source)、代理(Delegate)。app
UICollectionViewLayout 负责管理 UICollectionViewLayoutAttributes,一个 UICollectionViewLayoutAttributes 对象管理着一个 CollectionView 中一个 Item 的布局相关属性。包括 Bounds、center、frame 等。同时要注意在当 Bounds 在改变时是否须要刷新 Layout, 以及布局时的动画。异步
UICollectionViewFlowLayout 是 UICollectionViewLayout 的子类,是系统提供给咱们一个封装好的流式布局的类。ide
这种流式布局须要区分方向,方向不一样,具体的 Line Spacing 和 Item Spacing 所表明的含义不一样,具体差别,能够经过上面的两张图进行区分。函数
由于流式布局其强大的适用性,因此在设计中这种布局方式被普遍使用。
数据源:顾名思义,提供数据的分组信息、每组中 Item 数量以及每一个 Item
的实际内容。
optional func numberOfSections(in collectionView: UICollectionView) -> Int
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
复制代码
delegate 提供了一些细颗粒度的方法:
还有一些视图的显示事件:
系统提供的 UICollectionViewFlowLayout
虽然使用起来方便快捷,可以知足基本的布局须要。可是遇到以下图的布局样式,显然就没法达到咱们所需的效果,这时就须要自定义 FlowLayout
了。
自定义 FlowLayout
并不复杂 ,有如下四步:
override var collectionViewContentSize: CGSize
复制代码
func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
复制代码
// 为每一个 invalidateLayout 调用
// 缓存 UICollectionViewLayoutAttributes
// 计算 collectionViewContentSize
func prepare()
复制代码
// 在 CollectionView 滚动时是否容许刷新布局
func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool
复制代码
经过以上的方法,咱们能够轻松实现自定义 layout
的布局。可是在实际开发中,有一个对性能提高很实用的小技巧很值得咱们借鉴。
一般,咱们获取当前屏幕上全部显示的 UICollectionViewLayoutAttributes
会这么写
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return cachedAttributes.filter { (attributes:UICollectionViewLayoutAttributes) -> Bool in
return rect.intersects(attributes.frame)
}
}
复制代码
采用以上的写法,咱们会遍历缓存了全部 UICollectionViewLayoutAttributes 的 cachedAttributes 数组。而随着用户的拖动屏幕,这个方法会被频繁的调用,也就是会作大量的计算。当 cachedAttributes 数组的量级达到必定的规模,对性能的负面影响就会很是明显,用户在使用过程当中会出现卡顿的负面体验。
苹果工程师采用的办法能够很好地解决这一问题。全部的 UICollectionViewLayoutAttributes 都按照顺序被存储在 cachedAttributes 数组中,既然是一个有序的数组,那么只要咱们经过二分查找,拿到任何一个在当前页面显示的 Attribures 对象,就能够以这个 Attribures 对象为中心,向前向后遍历查找符合条件的 Attribures 对象便可,这样查找的范围就被大大缩小了。相应地,计算量变小,对性能的提高很是明显。
为了让你们易于理解,画了一张图,虽然有点丑,但表达思想足够了。 当前显示的 CollectionView 的范围就是 rect。在 rect 内部经过二分查找,找到第一个合适的 UICollectionViewLayoutAttributes 做为 firstMatchIndex,也就是那个 Attributes 对象。
在 rect 内, firstMatchIndex 以上的 Attributes 都符合 attributes.frame.maxY >= rect.minY
,而在 firstMatchIndex 如下的 Attributes 也都符合 attributes.frame.maxY <= rect.maxY
的条件。
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributesArray = [UICollectionViewLayoutAttributes]()
// 找到在当前区域内的任何一个 Attributes 的 Index
guard let firstMatchIndex = binarySearchAttributes(range: 0...cachedAttributes.endIndex, rect:rect) else { return attributesArray }
// 从后向前反向遍历,缩小查找范围
for attributes in cachedAttributes[..<firstMatchIndex].reversed {
guard attributes.frame.maxY >= rect.minY else {break}
attributesArray.append(attributes)
}
// 从前向后正向遍历,缩小查找范围
for attributes in cachedAttributes[firstMatchIndex...] {
guard attributes.frame.minY <= rect.maxY else {break}
attributesArray.append(attributes)
}
return attributesArray
}
复制代码
经过二分查找的方式,在处理当前页面显示的 UICollectionViewLayoutAttributes
的过程当中能够减小遍历的数据量,在实际体验中页面滑动更加顺滑,体验更好,这种处理 Attribures
对象的方式,值得咱们在开发过程当中借鉴。
咱们会遇到对 CollectionView
进行编辑的场景,编辑操做通常是新增、删除、刷新、插入等。在本 Session 中,主讲人为咱们作了一个示例。
为了便于理解,仍是贴一下代码吧:
// 原函数
func performUpdates() {
people[3].isUpdated = true
let movedPerson = people[3]
people.remove(at:3)
people.remove(at:2)
people.insert(movedPerson, at:0)
// Update Collection View
collectionView.reloadItems(at: [IndexPath(item:3, section:0)])
collectionView.reloadItems(at: [IndexPath(item:2, section:0)])
collectionView.moveItem(at: IndexPath(item:3, section:0), to:IndexPath(item:0, section:0))
}
复制代码
这个例子在操做过程当中报错,缘由以下:咱们删除和移动的是同一个索引位置的元素。咱们显示地调用了 reloadData()
, reloadData()
是一个异步执行的函数,会直接访问数据源方法,进行从新布局,屡次调用容易出错,同时这样写也没有动画效果。
上面出错的场景其实挺常见,为了规范操做,避免在编辑的场景下出现问题,应当将对 CollectionView
的新增、删除、刷新、插入等操做都放入到 performBatchUpdates()
中的 updates
闭包内,CollectionView
中 Item 的更新顺序咱们不须要关心,可是数据源更新的顺序是很重要的。
首先认识一下这个方法
func performBatchUpdates(_ updates: (() -> Void)?, completion: ((Bool) -> Void)? = nil)
1.其中 updates 闭包内部会执行新增、删除、刷新、插入等一系列操做。
2.而 completion 闭包会在 updates 闭包执行完毕后开始执行,updates 闭包中的相关操做会触发一些动画,
当这些动画执行成功会返回 True,当动画被打断或者执行失败会返回 false,这个参数也有可能会返回 nil。
复制代码
这个方法能够用来对 collectionView
中的元素进行批量的新增、删除、刷新、插入等操做,同时将触发collectionView
的 layout
的对应动画:
1.func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> NSCollectionViewLayoutAttributes?
2.func initialLayoutAttributesForAppearingDecorationElement(ofKind elementKind: NSCollectionView.DecorationElementKind, at decorationIndexPath: IndexPath) -> NSCollectionViewLayoutAttributes?
3.func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> NSCollectionViewLayoutAttributes?
4.func finalLayoutAttributesForDisappearingDecorationElement(ofKind elementKind: String, at decorationIndexPath: IndexPath) -> UICollectionViewLayoutAttributes?
复制代码
缘由是由于在执行完 performBatchUpdates
操做以后,CollectionView 会自动 reloadData
调用数据源方法从新布局。因此咱们在 Updates 闭包中对数据的编辑操做执行完毕后,必定要同步更新数据源,不然有极大的概率出现数据越界等错误状况。
既然在执行操做时容易出现问题,咱们就该想办法去规避,苹果的工程师给出了很好的建议。在上面咱们讲过对 CollectionView
的新增、删除、刷新、插入等操做都放入到 performBatchUpdates()
中的 updates
闭包内,CollectionView
中 Item 的更新顺序咱们不须要关心,可是数据源更新的顺序很重要。最后的 Item 更新顺序和数据源的更新顺序是怎么回事呢?
你能够这样理解:
而后咱们将刚才出错的代码,改成以下:
// 新的实现
func performUpdates() {
UIView.performWithoutAnimation {
// 先将数据刷新
CollectionView.performBatchUpdates({
people[3].isUpdate = true
CollectionView.reloadItems(at: [IndexPath(item:3, section:0)])
})
// 再将移动拆分红删除以后再插入两个动做
CollectionView.performBatchUpdates({
let movedPerson = people[3]
people.remove(at: 3)
people.remove(at: 2)
people.insert(movedPerson, at:0)
CollectionView.deleteItems(at: [IndexPath(item:2, section:0)])
collectionView.moveItem(at: IndexPath(item:3, section:0), to:IndexPath(item:0, section:0))
})
}
}
复制代码
最后总结一下,苹果的工程师建议咱们经过自定义布局来实现精美的布局样式,同时采起二分查找的方式来高效的处理数据,提高界面的流畅性和用户体验。
其次对 CollectionView 的操做建议咱们经过 performBatchUpdates
来进行处理,咱们不须要去考虑动画的执行,由于默认都帮助咱们处理好了,咱们只须要注意数据源处理的原则和顺序,确保数据处理的安全与稳定。
若是对这篇 Session 很感兴趣的话,能够在 Twitter 上联系做者,只须要在 Twitter 搜索 A Tour Of CollectionView 便可,做者仍是很热心的。
最后声明,笔者的英语听力比较惨,有些地方听得不是特别明白,一旦发现个人信息有遗漏或者传达的信息有误,还望你们不吝指教。
查看更多 WWDC 18 相关文章请前往 老司机x知识小集xSwiftGG WWDC 18 专题目录