iOS 中的 CompositionalLayout 、 DiffableDataSource(更新至iOS14)

CollectionView 相关内容:

1. iOS 自定义图片选择器 3 - 相册列表的实现
2. UICollectionView自定义布局基础
3. UICollectionView自定义拖动重排
4. 本文
5. iOS14 中的UICollectionViewListCell、UIContentConfiguration 以及 UIConfigurationState

前言:
iOS13 之前, CollectionView 实现主要依靠 Delegate, DataSource,Layout,三者通力协作以实现各种各样的布局类型。
随着越来越多的应用界面越来越复杂,实现起来耗时耗力,相似的界面因细微差别却需要重新写大量业务功能类似的代码。而这些界面都有一个共同点:
【界面元素“模块化”】
类似 AppStore、各种资讯 APP 的主页一样,界面被区分为多个区域,每个区域有自己单独的布局特点,苹果 iOS13 中新增并改良了不少的特性,以适应新的业务场景。本文主要以CollectionView为例介绍这些新的特性与使用方式。UITableView中也有对应的UITableViewDiffableDataSource,使用方法一样。


UICollectionViewCompositionalLayout

与 UICollectionViewFlowLayout 一样,UICollectionViewCompositionaLayout 也是基于 UICollectionViewLayout 的布局,比 FlowLayout 的实现复杂,也更加灵活。在界面模块化的场景下更加灵巧。逻辑更清晰。

CompositionalLayout 中,布局主要被划分为了item, group,section ,这三部分组合成 CompositionalLayout 基本结构,如图:

布局结构

item:可以理解为UICollectionViewCell,布局的最小单元。
group: 布局组合层,用于组合 item 的布局,其自身也能够嵌套(把被嵌套的group当成一个item进行布局),为布局提供更多可能。有垂直、水平、自定义三种方式,绘制时group并不会对视图层级造成影响。
section: 布局中每一段的布局定义,是group的容器,还提供了header、footer、附加视图等功能。可通过orthogonalScrollingBehavior 指定 section 的滚动方式

举一个简单的 Banner 布局的例子熟悉下上述各部分内容:


Banner效果图

从图中可以看出,Banner 在 CollectionView 的第一栏中,能够左右滑动,这在之前实现起来稍显复杂,嵌套 CollectionView 或是实现自定义 Scrollview 进行大量状态控制。而现在,其布局代码非常简单:

//因以模拟器举例,布局为绝对数值,实际开发中要注意不同屏幕的适配
var layout: UICollectionViewCompositionalLayout! = nil
var sectionProvider = { (index: Int, enviroment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
    // item(蓝色矩形,绝大大小 300x200)
    let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(300), heightDimension: .absolute(200))
    let item = NSCollectionLayoutItem(layoutSize: itemSize)

    // group(组合所有item,并设置gorup的内边距)
    let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(320), heightDimension: .absolute(200))
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
    group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10)
    
    // section(设置滚动方向)
    let section = NSCollectionLayoutSection(group: group)
    section.orthogonalScrollingBehavior = .groupPagingCentered
    return section
}
//······
layout = UICollectionViewCompositionalLayout(sectionProvider: sectionProvider)

上述代码中的item,group设置大小均用到了.absolute(XXX)。属于NSCollectionLayoutDimension 的类方法,该类提供了多种描述视图相对布局的方法:

【.fractionalWidth、.fractinalHeight】:
相对于容器宽/高的比例,例如:1表示与容器相等,0.5则表示是容器的一半。
【.absolute】:绝对数值
【.estimated】:估算大小

Tips:这里要注意 .fractionalWidth 与 .fractinalHeight 相对于容器的概念,在 CompositionalLayout 布局中,item的相对容器,应当是其加入的group,group相对容器,应当是其加入的 section 或者另一个 group,

group 可以管理 item 的布局,如间隔,内间距等等,因为 Banner 是横向滚动,所以使用了group的水平初始化方法 NSCollectionLayoutGroup.horizontal,其创建了一个水平布局的 group. 对应的是垂直布局。

section 根据 group 初始化,并指定了当前 section 的翻页方式,而在实际开发中,section 还能做到更多,例如添加附加视图等。

一个Banner,总是差点意思,我们可以再实现一个稍微复杂一点的布局:


布局图

这样的布局,可以按照两个Cell来做,这里我们尝试用 CompositionalLayout 来实现。

// 右侧小item
let smallItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(0.4))
let smallItem = NSCollectionLayoutItem(layoutSize: smallItemSize)
smallItem.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 0)

// 右侧group容器
let smallGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3), heightDimension: .fractionalHeight(1))
let smallGroup = NSCollectionLayoutGroup.vertical(layoutSize: smallGroupSize, subitem: smallItem, count: 2)
smallGroup.interItemSpacing = NSCollectionLayoutSpacing.fixed(10)

// 左侧大item
let bigItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.7), heightDimension: .fractionalHeight(1))
let bigItem = NSCollectionLayoutItem(layoutSize: bigItemSize)

// 容器group(包含了右侧group)
let bigGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(180))
let bigGroup = NSCollectionLayoutGroup.horizontal(layoutSize: bigGroupSize, subitems: [bigItem, smallGroup])
let section = NSCollectionLayoutSection(group: bigGroup)
section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 40, bottom: 20, trailing: 40)
section.interGroupSpacing = 20

// 设置背景卡片
let sectionBackgroundDecoration = NSCollectionLayoutDecorationItem.background(
    elementKind: CardBackViewKind)
sectionBackgroundDecoration.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 20, bottom: 5, trailing: 20)
section.decorationItems = [sectionBackgroundDecoration]
return section

上面使用相对布局来设定各部分组件的大小,并利用group的可嵌套性完成局部的自定义布局。
此处需要注意的是背景视图需要注册,与表头等附加视图在collectionView上注册不同,装饰视图是在layout上注册

layout.register(CardBackView.self, forDecorationViewOfKind: CardBackViewKind)



UICollectionViewDiffableDataSource

iOS13之前,用 UICollectionViewDataSource 来设置 CollectionView 有几行,每行有多少元素,Cell、header等等属性。其胜在简易灵活,但当我们频繁更新数据时,reloadData 太过暴力,尤其在需要动画过渡时,用户体验较差。
iOS13 中新增了 UICollectionViewDiffableDataSource 来帮助我们实现相应的功能。

可以看到,在 DiffableDataSource 中有跟 UICollectionViewDataSource 一样的方法:

@objc open func numberOfSections(in collectionView: UICollectionView) -> Int
@objc open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
@objc open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
@objc open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView

可以将 DiffableDataSource 像以前 UICollectionViewDataSource 一样类似的方式使用。但若如此的话 DiffableDataSource 也没有必要当做一门新特性推出了。在 DiffableDataSource 有一个提交方法:

open func apply(_ snapshot: NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType>, animatingDifferences: Bool = true, completion: (() -> Void)? = nil)

apply 方法提交了一个 NSDiffableDataSourceSnapshot 的结构体...该结构体描述了当前数据源的状态,有多少行,多少列。调用 apply 方法提交新的数据源简要(snapshot)或变更,程序就会根据 snapshot 更新 collectionView 的状态。


增加Item

实现这样的效果代码如下:

var updateSnap = dataSource.snapshot(for: "News")
updateSnap.append([dataSource.snapshot().numberOfItems + 1])

// 此处为 NSDiffableDataSourceSectionSnapshot,iOS14新增特性,可以对指定的单个 section 的数据源进行管理。
dataSource.apply(updateSnap, to: "News", completion: nil)

上面使用append将新数据追加在末尾,也可以使用insert或delete更改数据源,提交后,系统会自动在对应位置插入或删除,并附带过渡动画。
数据源简要更新方式具有“简易、自动化、差异化更新”的特点,原本需要开发者计算的状态变化交由系统完成,开发者只需要提供最新的数据源即可。

对于普通场景使用 NSDiffableDataSourceSnapshot 时,可以通过其提供的快捷属性来提供 Cell 或附加视图的代理(CellProvider 与 SupplementaryViewProvider)。直接在
DiffableDataSource 初始化时就设置Cell的代理也很简便。

dataSource = UICollectionViewDiffableDataSource<String, Int>(collectionView: collectionView) { (collectionView, indexPath, _) -> UICollectionViewCell? in
    switch indexPath.section {
    case 0:
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BannerCellID, for: indexPath)
        cell.backgroundColor = .blue
        return cell
    case 1:
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NewsCellID, for: indexPath)
        cell.backgroundColor = .orange
        return cell
    default:
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: GirlsCellID, for: indexPath)
        cell.backgroundColor = .systemPink
        return cell
    }
}

CompositionalLayout 还有补充视图(SupplementaryItem)Header 和 Fotter(BoundarySupplementaryItem)、以及本文用来当做卡片背景的装饰视图(DecorationItem),更多的内容可以下载官方的 Demo 来查看具体的代码实现。
关于 CompositionalLayout,DiffableDataSource 的简易介绍就到这里了,前者是苹果提供的官方布局,帮开发者省去了不少的工作量,后者是一种新的数据管理方式。

多说一句:这两个新增特性特点再结合最近苹果对 SwiftUI 的极力推崇,可以看出苹果对打通Mac iPad iPhone的决心,以及很早就开始的准备。而完全打通所有平台最快是明年,到时候应该还会新增一些特性,不过大体上的架构应该不会再变了,现在就熟悉这些特性正好合适

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271