iOS Modern Collection Views

本文是对 WWDC 2019、WWDC 2020 引入的 UICollectionView 新特性的理解。

关键词:

  • UICollectionViewCompositionalLayout
  • UICollectionViewDiffableDataSource
  • UIContentConfiguration

预备知识:熟练使用 UICollectionView,例如编写瀑布流等自定义布局

Introduction

  • 现有的 Collection Views 用 UICollectionViewFlowLayout 流式布局,局限性太高;自己实现 Layout 子类,代码太繁琐。iOS 13(WWDC 2019)开始引入 UICollectionViewCompositionalLayout,组合式布局,丰富了开发者的选择,iOS 14 又完善了大量功能。
  • 现有的 UICollectionViewDataSource 在面对复杂的内容增删时非常容易出错,iOS 13 开始引入 DiffableDataSource,并持续扩充新特性。
  • 新的构造 Cell 的思路:组合代替继承;新的状态更新机制。

配合本文的代码链接, 密码: 51b7。运行代码中的 iOS 项目,按照界面顺序浏览对应的源码,有了大致认识后读下文。

UICollectionViewCompositionalLayout

组合式布局,继承自 UICollectionViewLayout

我们已经熟悉一个 UICollectionView,由一或多个 Section 构成,各个 Section 内的布局方式可能一样,也可能不一样。我们通常处理这种情况时,根据 indexPath.section 返回对应的 itemSize 等布局属性。

现在,在组合式布局中,一个 Section 内的布局由一个 NSCollectionLayoutSection 对象描述,各个 Section 的布局对象“组合“成一个 UICollectionViewCompositionalLayout 整体布局对象。(后面分别简称 Section 对象,Layout 对象)

// 如果各个 Section 的布局相同,或者只有一个 Section,构造 Layout 对象时可以直接用:
init(section: NSCollectionLayoutSection)

// 如果 Section 存在不同布局,或者需要读取上下文信息,则使用:
init(sectionProvider: 闭包)
// 闭包类型如下,上下文中包含当前 traitCollection、父容器的 size、inset
(sectionIndex: Int, context: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection?

各个 Section 的布局有 Section 对象描述,Layout 对象仅需要描述“最外层”的属性,包括:滑动方向(主轴);Section 之间的 spacing; 整体的 inset,整体的 Boundary Supplementary Items(稍后介绍,先当作 tableHeaderView 的布局描述)。这部分属性,封装为一个 UICollectionViewCompositionalLayoutConfiguration 对象,可以当作上面两个构造函数的可选参数。

通过以上属性,整体 Layout 已经描述完成,下面开始描述每个 Section 如何布局。

NSCollectionLayoutSection

在 Flow Layout 中,Section 是由一个个 item 堆放起来的,一行空间用完再换一行,沿着主轴方向排列下去,这也是“flow”的意义。

在组合式布局中,我们把一个或多个 item 视为一个整体 group。这个整体,在一个 Section 中不断重复,可以视为,group 以 flow 模式堆放,不同点之一是,group 只是逻辑概念,不在真实视图层级上。

group 类 NSCollectionLayoutGroup,用于描述它内部如何摆放一个或多个 item;构造 group 时还需要声明自己的大小:可以是固定值,或者是被撑开的,或者是所在 UICollectionView 的宽高的倍数(父容器,不一定是 Collection View,后面会详述)。

构造 Section 时传递一个 group 对象;Section 对象不知道 item 的布局是什么样的,它只知道如何堆放 group:
在构建 Layout Attributes 时,对于某个 indexPath 的 item,让当前 group 尝试摆放,如果剩余空间能容纳该 item,则让 group 给出摆放位置 frame,构造 Layout Attributes;如果当前 group 空间已满,则清空/新建一个 group,让它给出摆放后的 frame。group 不断堆放,Section 大小不断更新,直到该 Section 的数据全摆放完毕。计算下一个 Section… 得到最终 UICollectionView 的 contentSize。

和 Layout 对象一样,Section 对象也有一些它那个“层级”的属性可以配置,如:Section 的 inset,group 的 spacing,Boundary Supplementary Items(稍后介绍,先当作 section header、section footer 的布局描述),Decoration Items(Section 的 decoration views 的布局描述)。还有一个非常牛逼的属性,正交滑动行为 orthogonalScrollingBehavior:假设 collection view 的主轴是垂直方向,前面提到 Section 堆放 group 时,像是 flow 模式,那么 flow 的方向,如果往主轴垂直(正交)的方向堆放呢(横向)?此时,系统会把该 Section 上的全部内容添加到一个 UIScrollView 上!现在堆放 group 过程中,Section 的高度 / Scroll View 的高度取最高 group 高度,Section 的宽度 / Scroll View 的宽度取 UICollectionView 宽度,Scroll View 的内容宽度(contentSize)不断累加 group 的宽度,最终效果是,该 Section 支持左右滑动,而主轴是垂直滑动!正交行为产生的 Scroll View 还支持配置翻页效果,页大小可以是 collection view 宽度,可以是 group 宽度;还支持停止位置:滑动后自动停到 group 的 leading / center 位置。

通过以上属性,Section 的布局描述完成。那么 group 是如何描述 item 的布局的呢?

NSCollectionLayoutGroup

前面提过,group 构造时,需要传递自己的大小。
那么,描述 item 的布局问题,就可以简化为:给定一个尺寸,返回一个 frame 数组。尺寸就是 group 大小,这些 frame,就是一个 group 里面所有 item 的位置大小。尺寸可以是一个大约值,实际值被 item 撑开,这种情况问题就简化为:给定一个不确定的尺寸,item 的大小是可预知的(固定值,或动态值如字符串的宽高),结合 item 的 spacing 及业务需求,就能反推出 group 的实际大小。

以上自定义 group 内 item 的布局,使用下面方法构造:

// 参数:group 大小 和 描述 item 布局的闭包,关于闭包:
// context:当前 traitCollection、父容器的 size、inset
// 返回的每个 item 只有两个属性,frame 和 zIndex(z 轴,一般用不到)
NSCollectionLayoutGroup.custom(layoutSize: NSCollectionLayoutSize) { 
	(context) -> [NSCollectionLayoutGroupCustomItem] in
	
}

但是一般 Collection View 用的最多的场景如,垂直方向滑动,一行展示 5 个。对于这种常见的布局,系统封装了两套工厂方法:水平/垂直方向布局 item。以水平方向为例:

class func horizontal(layoutSize: NSCollectionLayoutSize, subitems: [NSCollectionLayoutItem]) -> Self 

一种通用的水平方向布局,item 尺寸肯定不是写死的,得由我们指定。NSCollectionLayoutItem 用来描述一个 item①。group 对象还有两个属性:Supplementary Items(视为 group header 的布局描述,稍后介绍),item 间的 spacing②。

需要递归地介绍几个类,①:

NSCollectionLayoutItem

构造 item 时,需要传递一个大小对象,NSCollectionLayoutSize。构造 NSCollectionLayoutSize 时,宽高都有三种选择:

  • 绝对值 absolute
  • 估计值 estimated:使用渲染时实际大小
  • 父容器的比例 fractional:前面提到构造 group 时,如果使用 fractional,则父容器是 collection view;对于 item,父容器是 group。

有一些特殊情况:如果 group、item 都使用固定大小,而 item 更大,那么所有 item 都不会显示;如果 group 使用估计大小,item 使用比例大小,那么 group 的估计大小会被当作绝对大小…等等。

item 对象可以设置自己的 contentInsets,edgeSpacing②,Supplementary Items(视为 item header,稍后介绍)。

回到水平布局,构造 group 时传递一个大小,一个 subitems 数组。item 数组代表 group 允许多种 item 一起排,例如 [Item1, Item2],在排列时,先摆一个 Item1 样式的,再摆一个 Item2 样式的,循环下去直到 group 空间用尽。假如正打算摆 Item2 (业务需求当前 indexPath 应该用 Item2 的布局描述),但是 group 剩余空间不足了,于是“新建”一个 group,注意,此时新的 group 按照 [Item1, Item2] 的顺序,当前 indexPath 将会用 Item1 的布局描述!!!当然这属于程序员编程时的错误,没有合理安排 group 和 item 的大小,是完全可以避免的。

水平布局还提供另外一个方法:在 group 内指定 item 个数。

class func horizontal(layoutSize: NSCollectionLayoutSize, subitem: NSCollectionLayoutItem, count: Int) -> Self

前面是不断堆放 item 直至 group 空间用尽;使用带有 count 参数的方法时,只允许摆放一种 item,而且一定会摆放 count 个。为了达成这种效果,构造 item 时声明的宽度将被忽略,由 group 指定。

垂直与水平 group 原理相同。

NSCollectionLayoutSpacing

关于 ②:group 设置 item 间的 spacing,以前都是一个固定值,现在可以指定一个最小值。构造 NSCollectionLayoutSpacing 类的对象,如 fixed(10),flexible(10)。前者是固定值 10;后者最小值 10,使用场景如:一个 group 可以堆放 5 个 item,但是数据源只有 4 个,那么这 4 个 item 的间距会自动增加,以消化掉剩余的 group 空间。

item 有一个 edgeSpacing 属性 NSCollectionLayoutEdgeSpacing,四个边都有一个 spacing,相当于 margin,可以设置不一样的值。

Group 嵌套

NSCollectionLayoutGroup 继承自 NSCollectionLayoutItem,可以当作普通 item 放在 group 里实现更复杂需求。

场景:构造一个 Section 的 group,尺寸是 1 倍宽,0.3 倍高,水平方向布局,不设置 count,自由堆放。items 包括 Item1 和 Group1,Item1 是 0.7 倍宽,1 倍高(因为父容器是 section 的 group);Group1 是 0.3 倍宽,1 倍高(父容器同理)。Group1 内部是垂直方向布局,固定 count 2,item 为 Item2,Item2 是 1 倍宽,1 倍高(高度被忽略,因为垂直方向指定了 count)。先不考虑 spacing,这个 Section 的 group 单元大致已经可以想象出来了:

       +-----------------------------------------------------+
       | +---------------------------------+  +-----------+  |
       | |                                 |  |           |  |
       | |                                 |  |   Item2   |  |
       | |                                 |  |           |  |
       | |                                 |  +-----------+  |
       | |               Item1             |                 |
       | |                                 |  +-----------+  |
       | |                                 |  |           |  |
       | |                                 |  |   Item2   |  |
       | |                                 |  |           |  |
       | +---------------------------------+  +-----------+  |
       +-----------------------------------------------------+
Supplementary Items

在之前的 UICollectionView 中,每个 Section 可以有 Header、Footer,即 Supplementary View 补充视图,现在,补充视图功能得到了大幅度扩展。

补充视图可以被固定(anchor)在:整体 Layout 上,Section 上,Item 上,Group 上(继承自 Item),而且允许有多个(数组)。涉及到三个类:描述 anchor 位置的类 NSCollectionLayoutAnchor;描述 Item / Group 的补充视图的类 NSCollectionLayoutSupplementaryItem(继承自 Item);描述 Section / Layout 的补充视图的类 NSCollectionLayoutBoundarySupplementaryItem(继承自前面那个)。下面分别介绍。

构造 NSCollectionLayoutSupplementaryItem 时,需要指定:大小;种类(自定义字符串,用于区分);位置(anchor),可以配置属性 zIndex。

描述位置可以用 1 或 2 个 anchor 对象。NSCollectionLayoutAnchor,锚定位置,支持 上 下 左 右 左上 右上 左下 右下 八个位置;构造时除了指定位置,可选设置 offset,offset 可以是固定点数,也可以是补充视图大小的比例。使用一个 anchor 对象,称为 containerAnchor,比如指定 右 + 上,效果是 item 的右上角和补充视图的右上角绑定;如果此时多使用一个 itemAnchor,指定 左 + 下,结合效果是 item 的右上角 和 补充视图的左下角绑定。这里命名有些 confusing,对于补充视图来说,它的 container 就是 NSCollectionLayoutItem,前面称为 item;itemAnchor 叫 selfAnchor 更容易理解。。

构造好 Supplementary Items,再构造 NSCollectionLayoutItem 作为参数传入;group 通过属性赋值。

关于子类 NSCollectionLayoutBoundarySupplementaryItem,可能是因为作为 Section / Layout 的补充视图灵活性比较低,不使用 anchor 了,改为 alignment,只能指定八个方向。多了一个 Bool 属性 pinToVisibleBounds,实现 header、footer 滑动时的吸附效果。

构造好 Boundary Supplementary Items,赋值给 Section / Layout。

关于数据源,如果用旧 dataSource 逻辑的话也完全 OK,前面提到构造 NSCollectionLayoutSupplementaryItem 时需要自定义 kind (String),这个 kind 将会在数据源里用来区分是哪种补充视图:

func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
// 如果是 Layout 的补充视图,indexPath 无意义,是 (0, 0)
// Section 的:(0, section)

后面会介绍新版 Data Source。

Decoration Items

类 NSCollectionLayoutDecorationItem 描述一个装饰视图的布局,目前仅 Section 支持设置装饰视图。系统提供了一个类方法直接构造作为背景的装饰视图,可以调整 inset、zIndex,然后设置 Section 的 decorationItems(数组)。

List

以下有空再写

UICollectionViewDiffableDataSource

NSDiffableDataSourceSnapshot

NSDiffableDataSourceSectionSnapshot

NSDiffableDataSourceTransaction

NSDiffableDataSourceSectionTransaction

CellRegistration

Cell Configurations

UIContentConfiguration

UIBackgroundConfiguration

UIConfigurationState

常用:List Configurations

UICellAccessory
indent
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值