UITableView 和 UICollectionView 是咱们开发者最经常使用的控件了,大量的流式布局须要这两个控件来实现,所以这两个控件也是 Apple 重点优化的对象。在往届 WWDC 中,咱们已经受益于 UITableViewDataSourcePrefetching 、优化版 Autolayout 等带来的性能提高,以及 UITableViewDragDelegate 带来的原生拖拽功能。今年,Apple 带来了全新的 Compositional Layout 。它将完全颠覆 UICollectionView 的布局体验,大大拓展 UICollectionView 的可塑性。数组
早期的 App 设计相对简单,使用 UICollectionViewFlowLayout 能够应付大多数使用场景。而随着应用的发展,愈来愈多的页面趋于复杂化,UICollectionViewFlowLayout 在面对复杂布局每每会显得力不从心,或者很是复杂,须要进行大量的计算和判断。而自由度更高的 UICollectionViewLayout 则有着更高的接入门槛,稍有不慎还容易出现各类各样的 bug 。 app
Item
,以及能够上下、左右滑动的交互。假如你是开发者,你会如何搭建这个 UI ?你可能会使用多个 UICollectionView 嵌套在一个 UIScrollerView 中,由于 UICollectionView 的滚动轴只能有一个(横向 / 竖向)。但若是我告诉你,在新版 iOS 13 中,这个页面只使用了一个 UICollectionView ,你会有什么感受。你必定很好奇它是怎么作到的。其中的秘密就是 Compositional Layout 。
Compositional Layout 是这次随 iOS 13 一同发布的全新 UICollectionView 布局。它的目标有三个:ide
为了达到上面这三个目标,Compositional Layout 在原有 UICollectionViewLayout Item
Section
的基础上,增长了一层 Group
的概念。多个 Item
组成一个 Group
,多个 Group
组成一个 Section
。布局
说了这么多,还不如上代码性能
// Create a List by Specifying Three Core Components: Item, Group and Section
let 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
的大小、间距等属性。fetch
如何解读上面这段代码?优化
Item
的高度为44定高,宽度是父视图(Group
)宽度的 100% 。Group
的尺寸描述使用了和 Item
彻底相同的的 size ,即高度为44定高,宽度是父视图(Section
)宽度的 100% 。Section
的宽度是 UICollectionView的宽度,高度默认为其 Group
全部元素渲染出来的总高度。经过上面的解析,你可以在脑中勾画出这个 UICollectionView 长什么样子吗?好吧,其实我也不能,但好在我可以跑一下代码看下实际但结果。动画
好吧,我认可这有点难。由于咱们看代码的顺序都是从上而下,但假如 Compositional Layout 层级的尺寸依赖于父视图,咱们就不得不结合父视图和自身的布局来推倒出最终的布局,这须要必定的空间想象力。ui
在上面这个例子中,每个 “UITableViewCell” 就是一个 Item
,也是一个 Group
,而整个 “UITableViewCell” 只包含了一个 Section
。spa
因此看到这里你必定会好奇,咱们为何须要 Group
这么一个东西?请保持耐心,要解答这个问题须要看到留到最后。
咱们先来谈谈最基础的核心布局。 在详细介绍 Compositional Layout 中用到的四大类以前,咱们须要先来了解一下,一个新的用于描述尺寸大小的类。
过去,咱们可使用 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) -> Self class 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 中发挥做用的。
class NSCollectionLayoutSize {
init(widthDimension: NSCollectionLayoutDimension,
}
复制代码
单纯用于描述 Item
的大小,使用到了上面介绍的 NSCollectionLayoutDimension。
class NSCollectionLayoutItem {
convenience init(layoutSize: NSCollectionLayoutSize)
var contentInsets: NSDirectionalEdgeInsets
}
复制代码
用于描述一个 Item
的完整布局信息,包含了上面的尺寸 NSCollectionLayoutSize ,以及边距 NSDirectionalEdgeInsets。
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
。
class NSCollectionLayoutSection {
convenience init(layoutGroup: NSCollectionLayoutGroup)
var contentInsets: NSDirectionalEdgeInsets
}
复制代码
用于描述 Section
布局信息。一样能够经过修改 contentInsets 来改变 Section
的边距。
以上就是用于描述 Compositional Layout 用到的四个类。经过对布局的精确描述,咱们就可以获得可塑性很是强的 UICollectionView 布局,而无需重写复杂的 UICollectionViewLayout 。不过,Compositional Layout 的可玩性还不止于此,若是想要进一步的自定义,须要使用到一些额外的高级布局技巧。
对于 Item 而言,咱们可能会有相似 iOS 桌面小圆点的需求。经过 NSCollectionLayoutAnchor ,咱们能够很容易的给 Item 添加自定义小控件。
// NSCollectionLayoutAnchor
let 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 添加自定义锚。
Headers 和 Footers 是也咱们常常用到的组件,此次 Compositional Layout 弱化了 Header 和 Footer 的概念,他们都是 NSCollectionLayoutBoundarySupplementaryItem
,只不过你能够经过描述其相对于 Section
的位置(top / bottom)来达到过去 Header 和 Footer 的效果。
// NSCollectionLayoutBoundarySupplementaryItem
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: "header", alignment: .top)
let footer = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: footerSize, elementKind: "footer", alignment: .bottom)
header.pinToVisibleBounds = true
section.boundarySupplementaryItems = [header, footer]
复制代码
pinToVisibleBounds
属性则是用来描述 NSCollectionLayoutBoundarySupplementaryItem
划出屏幕后是否留在 CollectionView 的最上端,也就是以前 Plain style
的 Header 样式。
有没有遇到过这样的 UI 需求?
// Section Background Decoration Views
let background = NSCollectionLayoutDecorationItem.background(elementKind: "background")
section.decorationItems = [background]
// Register Our Decoration View with the Layout
layout.register(MyCoolDecorationView.self, forDecorationViewOfKind: "background")
复制代码
经过NSCollectionLayoutDecorationItem
,咱们能够为 Section
的背景添加自定义视图,其加载方式和 Item
Header
Footer
同样,须要先 register
。
在添加了如此多自定义特性以后,Compositional Layout 依旧支持自适应尺寸。这极大方便了咱们对动态内容的展现,同时对 Dynamic text 这类系统特性也能有更好的支持。
// Estimated Self-Sizing
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(44.0))
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize,
header.pinToVisibleBounds = true
elementKind: "header",
alignment: .top)
section.boundarySupplementaryItems = [header, footer]
复制代码
不知道你有没有发现,NSCollectionLayoutGroup
初始化方法中的 subitems
参数类型为 NSCollectionLayoutItem
数组,而 NSCollectionLayoutGroup
一样继承自 NSCollectionLayoutItem
,也就是说,NSCollectionLayoutGroup
内能够嵌套 NSCollectionLayoutGroup
。这样做的目的是,经过嵌套 Group
咱们能够自定义出层级更加复杂的布局。
这个 Group 用代码如何描述?
// Nested NSCollectionLayoutGroup
let 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 咱们能够很是直观的描述这一布局。
这个特性就是咱们前面提到的,让 Section 能够滚动起来的特性。
// Orthogonal Scrolling Sections
section.orthogonalScrollingBehavior = .continuous
复制代码
经过设置 Section 的 orthogonalScrollingBehavior 参数,咱们能够实现多种不一样的滚动方式。
// Orthogonal Scrolling Sections
enum UICollectionLayoutSectionOrthogonalScrollingBehavior: Int {
case none
case continuous
case continuousGroupLeadingBoundary
case paging
case groupPaging
case groupPagingCentered
}
复制代码
orthogonalScrollingBehavior
参数是一个 UICollectionLayoutSectionOrthogonalScrollingBehavior
类型的枚举,包含了咱们在实际开发者会用到的几乎全部滚动方式,好比常见的自由滚动,按page滚动,以及按 Group 滚动(包含以 Group Leading 为边界和以 Group Center 为边界)。以往要实现相似的效果,咱们大多须要本身实现 UICollectionViewLayout 或者干脆求助相似 AnimatedCollectionViewLayout 这样的第三方库,现在 Apple 已经为你所有实现!
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
。
Compositional Layout 为咱们带来了更加可塑易用的 CollectionView 布局以及多维度瀑布流,对于 UICollectionView 而言是一个全新的升级,它将赋予 UICollectionView 更多的可能性。一个注意的点是,iOS 13上的 App Store 已经用上了新的 Compositional Layout ,不过在 iPad 上旋转动画的性能不是很好,可见目前版本的 Compositional Layout 还有待优化的控件。不过限于 iOS 13 的版本限制,咱们还须要一段时间才能真正用上它,但我已经等不及了。
官方的Demo,几乎展现了Compositional Layout 的全部布局,支持 iOS 和 macOS。强烈推荐你们跟着代码和结果走一遍!
Using Collection View Compositional Layouts and Diffable Data Sources