美文网首页
iOS 中的 CompositionalLayout 、 Dif

iOS 中的 CompositionalLayout 、 Dif

作者: 猴子的饼干 | 来源:发表于2020-07-02 11:24 被阅读0次

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

    相关文章

      网友评论

          本文标题:iOS 中的 CompositionalLayout 、 Dif

          本文链接:https://www.haomeiwen.com/subject/ohfofktx.html