iOS13 Compositional Layout

前言

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

背景

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

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

介绍

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

  1. Composable 可组合的
  2. Flexible 灵活的
  3. Fast 快

为了达到上面这三个目标,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)
复制代码

能够看到,为了可以将复杂的布局描述清楚,咱们须要建立多个类来分别描述 ItemGroupSection 的大小、间距等属性。fetch

如何解读上面这段代码?优化

  1. 首先 Item 的高度为44定高,宽度是父视图(Group)宽度的 100% 。
  2. Group 的尺寸描述使用了和 Item 彻底相同的的 size ,即高度为44定高,宽度是父视图(Section)宽度的 100% 。
  3. Section 的宽度是 UICollectionView的宽度,高度默认为其 Group 全部元素渲染出来的总高度。
  4. 最终,咱们会经过 Frame 或 AutoLayout 对 UICollectionView 进行尺寸设置。

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

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

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

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

因此看到这里你必定会好奇,咱们为何须要 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) -> 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 中发挥做用的。

  1. NSCollectionLayoutSize

    class NSCollectionLayoutSize {
        init(widthDimension: NSCollectionLayoutDimension,
    }
    复制代码

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

  2. NSCollectionLayoutItem

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

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

  3. 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

  4. NSCollectionLayoutSection

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

    用于描述 Section 布局信息。一样能够经过修改 contentInsets 来改变 Section 的边距。

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

高级布局

NSCollectionLayoutAnchor

对于 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 添加自定义锚。

NSCollectionLayoutBoundarySupplementaryItem

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 样式。

NSCollectionLayoutDecorationItem

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

以往要实现这样的样式每每会很是复杂,而现在咱们终于能够自定义 Section 的背景啦。

// 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

Estimated Self-Sizing

在添加了如此多自定义特性以后,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]
复制代码

Nested NSCollectionLayoutGroup

不知道你有没有发现,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 咱们能够很是直观的描述这一布局。

Orthogonal Scrolling Sections

这个特性就是咱们前面提到的,让 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 已经为你所有实现!

而若是我但愿作一个相似 App Store 中部这样滚动的布局呢?

这会稍稍有些复杂。首先,若是你仔细阅读文档,你会发现 NSCollectionLayoutGroup 有一个咱们以前没有提到的 API 。

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

它相比默认的 API ,subitem 再也不接收数组而只接收单一的 Item (意味着这个模式下,Group 不支持多种大小的 ItemItem + 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 ,须要一个额外的层级来描述位于 SectionItem 的中间层。这样说可能会略显生涩,你们能够把如今的 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。强烈推荐你们跟着代码和结果走一遍!

Using Collection View Compositional Layouts and Diffable Data Sources

相关文章
相关标签/搜索