前言
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 特别感谢