WWDC2018 session 225

一、引言

Session 225 《A Tour of UICollectionView》从三方面来对 UICollectionView进行讨论,分别是 LayoutsUpdates以及 Animations

在正式开始讨论之前,先了解一下 UICollectionView的三个重要概念: LayoutData source以及 Delegate

二、UICollectionView相关重要概念

1. Layout

UICollectionView的布局有以下几个特点:

  • 所有内容显示的信息都被指定在 UICollectionViewLayoutAttributes中。
  • 提供了无效化机制来允许在显示过程中改变布局。
  • UICollectionView改变布局的时候,会显示动画。

UICollectionView的布局被抽象成了 UICollectionViewLayout类,该类是一个抽象类,不可直接使用,但系统提供了一个 UICollectionViewLayout的子类 UICollectionViewFlowLayout来供开发者使用。

UICollectionViewFlowLayout

  • UICollectionViewFlowLayoutUICollectionViewLayout的子类。
  • UICollectionViewFlowLayout扩展了 UICollectionViewDelegate
  • UICollectionViewFlowLayout是基于线性布局的。

那么 UICollectionViewFlowLayout是如何基于线性布局的呢?

若布局方向为竖直的,那么 UICollectionViewFlowLayout的布局方式如下图:

Flow Layout竖直布局

其中,与 mermaid flowchat Layout相关的两个重要概念为 Line SpacingInter-Item Spacing,它们在竖直布局中的表现如下图:

  • Line Spacing

Flow Layout竖直布局Line Spacing

  • Inter-Item Spacing

Flow Layout竖直布局Inter-Item Spacing

若布局方向为水平,那么 UICollectionViewFlowLayout的布局方式如下图:

Flow Layout水平布局

其中, Line SpacingInter-Item Spacing在水平布局中的表现如下图:

  • Line Spacing

Flow Layout水平布局Line Spacing

  • Inter-Item Spacing

Flow Layout水平布局Inter-Item Spacing

UICollectionViewFlowLayout中,我们可以指定 Line SpacingInter-Item Spacing的最小值。

2. Data source

Layout指定了 UICollectionView如何显示内容,而内容则是由 Data source指定的。

Data source涉及到的有关方法如下:

// 指定section个数,可选的,默认1个section
optional func numberOfSections(in collectionView: UICollectionView) -> Int

// 指定section中item个数
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int

// 指定item显示内容
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell

3. Delegate

关于 UICollectionViewDelegate,有以下几个特性:

  • Delegate是可选的。
  • UICollectionViewDelegate是对 UIScrollViewDelegate的拓展。
  • 可以完成一些细粒度控制,比如对高亮和选中进行控制。
  • 可以对内容出现在屏幕过程进行控制,例如 willDisplayItemdidEndDisplayingItem

三、简单使用自定义的 UICollectionViewLayout

对于自定义的 UICollectionViewLayout,我们只需简单的重写它的 prepareLayout方法即可。

prepareLayout方法中,我们只需指定 itemSizesectionInsetsectionInsetReference等属性就能完成自定义。

prepareLayout会在 UICollectionView布局无效化的时候调用,那么 UICollectionView无效化如何触发呢?当 UICollectionViewbounds发生变化时(例如改变 UICollectionView大小,旋转屏幕等),此时会调用 prepareLayout

四、完整的自定义 UICollectionViewLayout

要想完整的自定义一个 UICollectionViewLayout,我们只需要重写4个方法以及根据需求实现一个额外方法就可以了。

1. 重写方法

open var collectionViewContentSize: CGSize { get }

该方法需要提供 UICollectionView所有内容所占区域大小。

由于 UICollectionView是继承自 UIScrollView的,该方法返回值与 UIScrollViewcontentSize是类似的。

func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?

func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?

上述两个方法都是需要返回 UICollectionView中内容布局信息,不同的是第一个方法需要根据区域返回,第二个方法需要根据具体排列位置返回。

以上两个方法在 UICollectionView滑动过程中会频繁调用,所以为了保证效率,不要在这两个方法中做复杂的数学计算,若无法避免进行计算,则应当考虑使用算法进行优化。

func prepare()

该方法在上面已经提到过了,正如同上面所说,该方法会在布局无效化的时候进行调用,所以在该方法中要做的处理,大致应该是以下几步:

  • 重置布局中的缓存数据。
  • 计算所有内容的布局。
  • 缓存计算好的布局。
  • 依据计算好的布局及时更新 contentSize,使之与布局保持同步。

2. 额外方法

func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool

对于该方法,首先需要注意以下几点:

  • 每次 UICollectionViewbounds发生变化时就会被调用。
  • UICollectionView滚动时会发生调用。
  • 默认实现为返回 false

该方法会将布局设置为无效,进而调用 prepare方法,所以在该方法中,根据具体情况进行设置,可以有效的减少 prepare方法的调用次数,最大可能的提高效率。

五、UpdateAnimations

首先假如我们要实现一个更新数据源的功能,在该功能中,我们为功能加上特定的动画,动画效果如下:

Update动画效果

在该更新动画中,由以下三个小部分更新动画组成:

  1. 重新加载最后一条数据内容。
  2. 将最后一条数据移动至第一条。
  3. 删除第三条数据。

正常的代码实现应当如下所示:

// MARK: Updates
func performUpdates() {
    
    collectionView.performBatchUpdates({
        // Update Data Source
        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.reloadItem(at: [IndexPath(item: 3, section: 0)])
        collectionView.deleteItem(at: [IndexPath(item: 2, section: 0)])
        collectionView.moveItem(at: IndexPath(item: 3, section: 0), to: IndexPath(item: 0, section: 0))
    })
}

以上代码的实现思路为:调整数据为最终效果所需数据,对应使用动画调整界面元素。

但是很不幸,以上代码会发生如下错误:

Update 错误

为什么会发生如上错误呢?我们首先来看一下performBatchUpdates方法的一些特点:

  1. 所有动画会一起执行。
  2. 在闭包中执行数据源的更新以及CollectionView视图的更新。
  3. CollectionView视图的更新顺序不重要。
  4. 数据源的更新顺序很重要。

既然提到了CollectionView视图的更新顺序不重要,那么出现问题的原因就在于数据源的更新顺序上了。

那我们看一下视图更新与数据源更新之间有什么关系:

视图更新与数据更新关系

由CollectionView的每个Action操作含义我们可以得出以下会引起错误的操作:

  1. Move和Delete同一位置元素。
  2. Move和Insert同一位置元素。
  3. Move多个元素到统一位置。
  4. 使用错误的位置元素。

那么我们应该如何避免出现以上错误操作呢?

  1. 将Move操作分解为Delete和Insert操作。
  2. 合并所有Delete和Insert操作。
  3. 先处理Delete操作,它们是按照降序处理的。
  4. 最后处理Insert操作,它们是按照升序处理的。

那对于reloadData来说,有什么特点呢:

  1. 无需更新操作。
  2. 能够同步数据源和CollectionView。
  3. 不需要动画。

根据以上建议,我们重新实现一下开始时候提到的需求了。

// MARK: Updates
func performUpdates() {
    
    // Perform reloads first
    UIView.performWithoutAnimation {
        collectionView.performBatchUpdates({
            people[3].isUpdated = true
            collectionView.reloadItems(at: [IndexPath(item: 3, section: 0)])
        })
    }
    
    collectionView.performBatchUpdates({
        // We have 2 updates:
        // - delete item at index 2
        // - Move item at index 3 to index 0
        
        // becomes...
        
        // delete item at index 2
        // delete item at index 3
        // insert item from index 3 at index 0
        
        let movedPerson = people[3]
        people.remove(at: 3)
        people.remove(at: 2)
        
        people.insert(movedPerson, at: 0)
        
        // Update Collection View
        collectionView.deleteItems(at: [IndexPath(item: 2, section: 0)])
        collectionView.moveItem(at: IndexPath(item: 3, section: 0), to: IndexPath(item: 0, section: 0))
    })
}

以上就是关于该篇Session提到的UICollectionView使用过程中的一些技巧及注意事项,如有遗留,欢迎补充。

发布了71 篇原创文章 · 获赞 34 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/TuGeLe/article/details/88399142