iOS13 Compositional Layout帮你实现collecion的各种布局

前言

UITableView 和 UICollectionView 是我们开发者最常用的控件了,大量的流式布局需要这两个控件来实现,因此这两个控件也是 Apple 重点优化的对象。在往届 WWDC 中,我们已经受益于 UITableViewDataSourcePrefetching 、优化版 Autolayout 等带来的性能提升,以及 UITableViewDragDelegate 带来的原生拖拽功能。今年,Apple 带来了全新的 Compositional Layout 。它将彻底颠覆 UICollectionView 的布局体验,大大拓展 UICollectionView 的可塑性。

背景

早期的 App 设计相对简单,使用 UICollectionViewFlowLayout 可以应付大多数使用场景。而随着应用的发展,越来越多的页面趋于复杂化,UICollectionViewFlowLayout 在面对复杂布局往往会显得力不从心,或者非常复杂,需要进行大量的计算和判断。而自由度更高的 UICollectionViewLayout 则有着更高的接入门槛,稍有不慎还容易出现各种各样的 bug 。

我们就拿 App Store为例,它包含了大小不一的 Item ,以及可以上下、左右滑动的交互。假如你是开发者,你会如何搭建这个 UI ?你可能会使用多个 UICollectionView 嵌套在一个 UIScrollerView 中,因为 UICollectionView 的滚动轴只能有一个(横向 / 竖向)。但如果我告诉你,在新版 iOS 13 中,这个页面只使用了一个 UICollectionView ,你会有什么感觉。你一定很好奇它是怎么做到的。其中的秘密就是 Compositional Layout 。


介绍

Compositional Layout 是此次随 iOS 13 一同发布的全新 UICollectionView 布局。它的目标有三个:

Composable 可组合的

Flexible 灵活的

Fast 快

为了达到上面这三个目标,Compositional Layout 在原有 UICollectionViewLayout Item Section 的基础上,增加了一层 Group 的概念。多个 Item 组成一个 Group ,多个 Group 组成一个 Section 。

说了这么多,还不如上代码

// Create a List by Specifying Three Core Components: Item, Group and Sectionlet size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),

                                  heightDimension: .absolute(44.0))let item = NSCollectionLayoutItem(layoutSize: size)let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitems: [item]) 

let section = NSCollectionLayoutSection(group: group)let layout = UICollectionViewCompositionalLayout(section: section)复制代码

可以看到,为了能够将复杂的布局描述清楚,我们需要创建多个类来分别描述 Item 、 Group 、 Section 的大小、间距等属性。

如何解读上面这段代码?

首先 Item 的高度为44定高,宽度是父视图(Group)宽度的 100% 。

Group 的尺寸描述使用了和 Item 完全相同的的 size ,即高度为44定高,宽度是父视图(Section)宽度的 100% 。

Section 的宽度是 UICollectionView的宽度,高度默认为其 Group 所有元素渲染出来的总高度。

最终,我们会通过 Frame 或 AutoLayout 对 UICollectionView 进行尺寸设置。

通过上面的解析,你能够在脑中勾画出这个 UICollectionView 长什么样子吗?好吧,其实我也不能,但好在我能够跑一下代码看下实际但结果。


结果就是一个类似 UITableView 的布局。

好吧,我承认这有点难。因为我们看代码的顺序都是从上而下,但假如 Compositional Layout 层级的尺寸依赖于父视图,我们就不得不结合父视图和自身的布局来推倒出最终的布局,这需要一定的空间想象力。

在上面这个例子中,每一个 “UITableViewCell” 就是一个 Item ,也是一个 Group ,而整个 “UITableViewCell” 只包含了一个 Section 。

所以看到这里你一定会好奇,我们为什么需要 Group 这么一个东西?请保持耐心,要解答这个问题需要看到留到最后。

核心布局

我们先来谈谈最基础的核心布局。 在详细介绍 Compositional Layout 中用到的四大类之前,我们需要先来了解一下,一个新的用于描述尺寸大小的类。

NSCollectionLayoutDimension

过去,我们可以使用 CGSize 来描述一个固定大小的 Item 。后来,我们拥有了 estimatedItemSize 来描述一个动态计算大小的 Item ,并且给它一个预估的值。但更多的时候,为了适配不同的屏幕尺寸,我们需要根据屏幕的宽度手动计算出 Item 的大小(比如限定一行只显示3个 Item )。

如何用简洁优雅的方式去描述上面三种场景呢?答案是 NSCollectionLayoutDimension

class NSCollectionLayoutDimension {class func fractionalWidth(_ fractionalWidth: CGFloat) -> Self class func fractionalHeight(_ fractionalHeight: CGFloat) -> Self class func absolute(_ absoluteDimension: CGFloat) -> Selfclass func estimated(_ estimatedDimension: CGFloat) -> Self}复制代码

NSCollectionLayoutDimension 添加了根据父视图的比例来描述尺寸的 fractionalWidth / fractionalHeight 的方法,并将定值、自适应、比例这三大描述方式统一分装了起来。

我们来看一个例子。

let size = NSCollectionLayoutDimension(widthDimension: .fractionalWidth(0.25), 

                                       heightDimension: .fractionalWidth(0.25))

}复制代码


如图,使用简单的描述,我们就可以得到以父视图(Item 的父视图为 Group)为基准的比例尺寸。它不仅可以被用于描述 Item 的大小,同样也可以用于 Group。

了解完这个基础之后,让我们看看 NSCollectionLayoutDimension 是如何在 Compositional Layout 中发挥作用的。

NSCollectionLayoutSize

class NSCollectionLayoutSize {init(widthDimension: NSCollectionLayoutDimension,

}复制代码

单纯用于描述 Item 的大小,使用到了上面介绍的 NSCollectionLayoutDimension。

NSCollectionLayoutItem

class NSCollectionLayoutItem {convenience init(layoutSize: NSCollectionLayoutSize)var contentInsets: NSDirectionalEdgeInsets}复制代码

用于描述一个 Item 的完整布局信息,包含了上面的尺寸 NSCollectionLayoutSize ,以及边距 NSDirectionalEdgeInsets。

NSCollectionLayoutGroup

class NSCollectionLayoutGroup: NSCollectionLayoutItem { class func horizontal(layoutSize: NSCollectionLayoutSize, subitems: [NSCollectionLayoutItem]) -> Self class func vertical(layoutSize: NSCollectionLayoutSize, subitems: [NSCollectionLayoutItem]) -> Self class func custom(layoutSize: NSCollectionLayoutSize, itemProvider: NSCollectionLayoutGroupCustomItemProvider) -> Self}复制代码

用于描述 Group 布局。它提供了垂直 / 水平两种方向。同时你也可以实现 NSCollectionLayoutGroupCustomItemProvider 自定义 Group 的布局方式。

它同样接收一个 NSCollectionLayoutDimension ,用于确定 Group 的大小。需要注意的是,当 Item 使用了 fractionalWidth / fractionalHeight 时, Group 的大小会影响 Item 的大小。

此外,它还有一个 subitems 参数,类型为 NSCollectionLayoutItem 数组,用于传递 Item 。

NSCollectionLayoutSection

class NSCollectionLayoutSection {convenience init(layoutGroup: NSCollectionLayoutGroup) var contentInsets: NSDirectionalEdgeInsets}复制代码

用于描述 Section 布局信息。同样可以通过修改 contentInsets 来改变 Section 的边距。

以上就是用于描述 Compositional Layout 用到的四个类。通过对布局的精确描述,我们就能够得到可塑性非常强的 UICollectionView 布局,而无需重写复杂的 UICollectionViewLayout 。不过,Compositional Layout 的可玩性还不止于此,如果想要进一步的自定义,需要使用到一些额外的高级布局技巧。

高级布局

NSCollectionLayoutAnchor

对于 Item 而言,我们可能会有类似 iOS 桌面小圆点的需求。通过 NSCollectionLayoutAnchor ,我们可以很容易的给 Item 添加自定义小控件。

// NSCollectionLayoutAnchorlet badgeAnchor = NSCollectionLayoutAnchor(edges: [.top, .trailing],

fractionalOffset: CGPoint(x: 0.3, y: -0.3))let badgeSize = NSCollectionLayoutSize(widthDimension: .absolute(20),

heightDimension: .absolute(20))let badge = NSCollectionLayoutSupplementaryItem(layoutSize: badgeSize, elementKind: "badge", containerAnchor: badgeAnchor)let item = NSCollectionLayoutItem(layoutSize: itemSize, supplementaryItems: [badge])复制代码

同样是通过多个类来分别描述 Anchor 的方位、大小和视图,我们就可以非常方便地为 Item 添加自定义锚。

NSCollectionLayoutBoundarySupplementaryItem

Headers 和 Footers 是也我们经常用到的组件,这次 Compositional Layout 弱化了 Header 和 Footer 的概念,他们都是 NSCollectionLayoutBoundarySupplementaryItem ,只不过你可以通过描述其相对于 Section的位置(top / bottom)来达到过去 Header 和 Footer 的效果。

// NSCollectionLayoutBoundarySupplementaryItemlet header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: "header", alignment: .top)let footer = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: footerSize, elementKind: "footer", alignment: .bottom)

header.pinToVisibleBounds = truesection.boundarySupplementaryItems = [header, footer]复制代码

pinToVisibleBounds 属性则是用来描述 NSCollectionLayoutBoundarySupplementaryItem 划出屏幕后是否留在 CollectionView 的最上端,也就是之前 Plain style 的 Header 样式。


NSCollectionLayoutDecorationItem


有没有遇到过这样的 UI 需求?

以往要实现这样的样式往往会非常复杂,而如今我们终于可以自定义 Section 的背景啦。

// Section Background Decoration Viewslet background = NSCollectionLayoutDecorationItem.background(elementKind: "background")

section.decorationItems = [background]// Register Our Decoration View with the Layoutlayout.register(MyCoolDecorationView.self, forDecorationViewOfKind: "background")复制代码

通过NSCollectionLayoutDecorationItem ,我们可以为 Section 的背景添加自定义视图,其加载方式和 ItemHeader Footer 一样,需要先 register 。

Estimated Self-Sizing

在添加了如此多自定义特性之后,Compositional Layout 依旧支持自适应尺寸。这极大方便了我们对动态内容的展示,同时对 Dynamic text 这类系统特性也能有更好的支持。

// Estimated Self-Sizinglet headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),

heightDimension: .estimated(44.0))let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize,

header.pinToVisibleBounds = trueelementKind: "header",

alignment: .top)

section.boundarySupplementaryItems = [header, footer]复制代码

Nested NSCollectionLayoutGroup

不知道你有没有发现,NSCollectionLayoutGroup 初始化方法中的 subitems 参数类型为 NSCollectionLayoutItem 数组,而 NSCollectionLayoutGroup 同样继承自 NSCollectionLayoutItem ,也就是说,NSCollectionLayoutGroup 内可以嵌套 NSCollectionLayoutGroup 。这样作的目的是,通过嵌套 Group我们可以自定义出层级更加复杂的布局。


这个 Group 用代码如何描述?

// Nested NSCollectionLayoutGrouplet leadingItem = NSCollectionLayoutItem(layoutSize: leadingItemSize) let trailingItem = NSCollectionLayoutItem(layoutSize: trailingItemSize)let trailingGroup = NSCollectionLayoutGroup.vertical(layoutSize: trailingGroupSize) subitem: trailingItem, count: 2)let containerGroup = NSCollectionLayoutGroup.horizontal(layoutSize: containerGroupSize, subitems: [leadingItem, trailingGroup])复制代码

想一想如此复杂的布局如果自己去实现 UICollectionViewLayout 将会是多么复杂,如今通过简洁而抽象的 Compositional Layout API 我们可以非常直观的描述这一布局。

Orthogonal Scrolling Sections

这个特性就是我们前面提到的,让 Section 可以滚动起来的特性。

// Orthogonal Scrolling Sectionssection.orthogonalScrollingBehavior = .continuous复制代码

通过设置 Section 的 orthogonalScrollingBehavior 参数,我们可以实现多种不同的滚动方式。

// Orthogonal Scrolling Sectionsenum UICollectionLayoutSectionOrthogonalScrollingBehavior: Int {case nonecase continuouscase continuousGroupLeadingBoundarycase pagingcase groupPagingcase groupPagingCentered

}复制代码

orthogonalScrollingBehavior 参数是一个 UICollectionLayoutSectionOrthogonalScrollingBehavior 类型的枚举,包含了我们在实际开发者会用到的几乎所有滚动方式,比如常见的自由滚动,按page滚动,以及按 Group 滚动(包含以 Group Leading 为边界和以 Group Center 为边界)。以往要实现类似的效果,我们大多需要自己实现 UICollectionViewLayout 或者干脆求助类似 AnimatedCollectionViewLayout 这样的第三方库,如今 Apple 已经为你全部实现!


而如果我希望做一个类似 App Store 中部这样滚动的布局呢?


这会稍稍有些复杂。首先,如果你仔细阅读文档,你会发现 NSCollectionLayoutGroup 有一个我们之前没有提到的 API 。

open class func vertical(layoutSize: NSCollectionLayoutSize, subitem: NSCollectionLayoutItem, count: Int) -> Self复制代码

它相比默认的 API ,subitem 不再接收数组而只接收单一的 Item (意味着这个模式下,Group 不支持多种大小的 Item 或 Item + Group 的组合,但聪明的你一定想到了可以先构建一个组合的 Group 然后传进这个 API 中),同时多了一个 count。这个 count 会让 Group 尝试在其限定的大小内塞入 count 个数的 Item 。最终达到的效果就是类似

let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item, item, item])复制代码

不过上面的代码不会生效,因为 subitems 关注的是不同的 Item 的组合,而非实际 Item 的个数,因此 subitems 会对数组内的 Item 去重。因此如果你希望在一个 Group 中塞入多个 Item,后者是你唯一的选择。

看到这里你是否对 Group 的作用有了一点感觉?上面的例子中,如果我们关闭 Section 的滚动功能,那么会是什么样子的?


每个 Group 中还是会有 3 个 Item,只不过由于 Section 的宽度限制,下一个 Group 不得不排布到上一个 Group 的下放,结果展示出来的还是一个类似 TableView 的布局。当我们打开 Section 的滚动模式,奇迹发生了。由于 Section 可以滚动,因此它存在类似于 ScrollerView 的 ContentView ,它的子 View 可以在更大的范围内渲染,因此之后的 Group 可以跟随在之前的 Group 右侧,并最终填充 Section 的整个 ContentView。

现在你该知道 Apple 为什么要引入 Group 的概念了吧。其实我在看Advances in Collection View Layout 的时候也是闷的,直到最后看到了 App Store 的例子我才明白了,为了能够实现多纬度的滚动(实际上是赋予了 Section 滚动的特性),原有的层级就不足以描述一个完整的多维度 CollectionView ,需要一个额外的层级来描述位于 Section 和 Item 的中间层。这样说可能会略显生涩,大家可以把现在的 Section 想象成原来的 CollectionView ,而新的 Group 就是原来的 Section。由于现在 Section 充当了之前 CollectionView 的角色被赋予了滚动的特性,因此需要一个额外的层级来描述之前 Section 所描述的  “一组 Item 的” 关系 。 Group 便由此出现。

可以说 Group 的存在是完全服务于这个可滚动 Section 的。可滚动的 Section 为 CollectionView 增加了一个纬度的信息流,如果你的 CollectionView 没有多维滚动的需求,那么你会发现 Compositional Layout 中 Group 的存在是一个完全没有必要的事情。

复习

正如我前面所说,Compositional Layout 的层级关系依次是 Item > Group > Section > Layout 。


理解了这其中的层级关系和特性,能够帮助你写出更灵活、性能更好的 UI !

总结

Compositional Layout 为我们带来了更加可塑易用的 CollectionView 布局以及多维度瀑布流,对于 UICollectionView 而言是一个全新的升级,它将赋予 UICollectionView 更多的可能性。一个注意的点是,iOS 13上的 App Store 已经用上了新的 Compositional Layout  ,不过在 iPad 上旋转动画的性能不是很好,可见目前版本的 Compositional Layout 还有待优化的控件。不过限于 iOS 13 的版本限制,我们还需要一段时间才能真正用上它,但我已经等不及了。

官方的Demo,几乎展示了Compositional Layout 的所有布局,支持 iOS 和 macOS。强烈推荐大家跟着代码和结果走一遍!

官方Demo点击这里

本篇文章引用http://www.cocoachina.com/articles/28730?filter=rec   特别感谢

你可能感兴趣的:(iOS13 Compositional Layout帮你实现collecion的各种布局)