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 基本结构,如图:

iOS 中的 CompositionalLayout 、 DiffableDataSource(更新至iOS14)_第1张图片
布局结构

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

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


iOS 中的 CompositionalLayout 、 DiffableDataSource(更新至iOS14)_第2张图片
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,总是差点意思,我们可以再实现一个稍微复杂一点的布局:


iOS 中的 CompositionalLayout 、 DiffableDataSource(更新至iOS14)_第3张图片
布局图

这样的布局,可以按照两个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, animatingDifferences: Bool = true, completion: (() -> Void)? = nil)

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


iOS 中的 CompositionalLayout 、 DiffableDataSource(更新至iOS14)_第4张图片
增加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更改数据源,提交后,系统会自动在对应位置插入或删除,并附带过渡动画。
数据源简要更新方式具有“简易、自动化、差异化更新”的特点,原本需要开发者计算的状态变化交由系统完成,开发者只需要提供最新的数据源即可。

iOS 中的 CompositionalLayout 、 DiffableDataSource(更新至iOS14)_第5张图片

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

dataSource = UICollectionViewDiffableDataSource(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的决心,以及很早就开始的准备。而完全打通所有平台最快是明年,到时候应该还会新增一些特性,不过大体上的架构应该不会再变了,现在就熟悉这些特性正好合适

你可能感兴趣的:(iOS 中的 CompositionalLayout 、 DiffableDataSource(更新至iOS14))