iOS14 中的 UICollectionViewListCell、UIContentConfiguration 以及 UIConfigurationState

CollectionView 相关内容:

1. iOS 自定义图片选择器 3 - 相册列表的实现
2. UICollectionView自定义布局基础
3. UICollectionView自定义拖动重排
4. iOS13 中的 CompositionalLayout 与 DiffableDataSource
5. 本文

前言:
上一篇对 CompositionalLayout、DiffableDataSource 做了介绍,基于这两个特性,本文是对上一篇文章的补充。

iOS13 开始,苹果推出 CompositionalLayout 涵盖了大部分常用布局的情况,又推出了 DiffableDataSource 替代了 之前使用 DataSource 来管理数据源的方式,并在其基础之上用 snapshot 模块化了模型管理。iOS14 中,新增了 snapshot 分段提交功能,也提供了众多官方玩具。例如UICollectionViewListCell,UIContentConfiguration,UIConfigurationState等。


UICollectionViewListCell

其实在两年前,就有人在 UIKit 中发现苹果用 CollectionView 实现 TableView 样式的痕迹,没想到现在才放出来,UICollectionViewListCell 是苹果为开发者封装好的在 CollectionView 中使用的 TableView 样式的Cell,其与 UITableViewCell 一样提供多种样式及对应的缺省组件,允许开发者对其进行配置。

在 iOS14 中,用 CollectionView 实现一个 TableView 样式的列表非常简单。

布局

let layout = UICollectionViewCompositionalLayout.list(using: UICollectionLayoutListConfiguration(appearance: .insetGrouped))

UICollectionViewCompositionalLayout.list() 是 tableView 样式,完全由苹果实现。UICollectionLayoutListConfiguration 为布局配置,此处仅指定了外观样式为 .insetGrouped,还有plain、grouped、sidebar、sidebarPlain。

使用 CellRegistration 注册 Cell

以前的 collectionView 都是用 .register(cellClass: forCellWithReuseIdentifier:) 来注册Cell,再在 dataSource 中进行复用。

CellRegistration 是 iOS14 中 CollectionView 新增的特性之一,还有注册附加视图的 SupplementaryRegistration。

具体使用方式:

// 注册Cell
let cellRegist = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { (cell, indexPath, item) in
    // 各种关于Cell的配置
}

// 在dataSource中获取注册的Cell
dataSource = UICollectionViewDiffableDataSource<String, Int>(collectionView: collectionView) { (collectionView, indexPath, item) -> UICollectionViewCell? in
    return collectionView.dequeueConfiguredReusableCell(using: cellRegist, for: indexPath, item: item)
}

日常开发中,当 Cell 的业务比较复杂时都会单独提供一个配置方法,以给 dataSource 的委托方法减负,并将功能模块化,这里的 CellRegistration 就能达到这样的效果,算是苹果顺手帮开发者解了下耦。而且,其利用泛型的特点,比使用字符串作为 CellID 更加的严谨。

有 Swift 那味了

使用 NSDiffableDataSourceSnapshot 提供数据

NSDiffableDataSourceSnapshot 是 iOS13 中对新增来对 dataSource 中的数据源进行管理的方式,DataSource 根据提交的 snapshot 自动识别模型变化并更新 collectionView。这一切都是自动的,高效的,低开销,丝滑的... ,有效避免了在使用 beginUpdate、insertItem 等方法中因开发者对数据源处理不够精确而引发的问题。

试试给我们的 collectionView 随便提供点初始化数据:

var snapshot = NSDiffableDataSourceSnapshot<String, Int>()
snapshot.appendSections(["Section1"])
snapshot.appendItems(Array(0..<10), toSection: "Section1")
dataSource?.apply(snapshot, animatingDifferences: true, completion: nil)

看看这点击效果、这 innerGroup 的外观效果,这左滑手势...等等...我们只是初始化了布局,注册了Cell,这些东西是系统帮我们实现的么?像左滑手势的回调又是什么?

这当然不是系统生成的,先看看 UICollectionViewListCell 里面有些什么:

// iOS14之后 可用
open class UICollectionViewListCell : UICollectionViewCell {

    // 缩进相关设置:
    open var indentationLevel: Int
    open var indentationWidth: CGFloat
    open var indentsAccessories: Bool
    
    open var separatorLayoutGuide: UILayoutGuide { get }

    // 滑动事件配置:
    open var leadingSwipeActionsConfiguration: UISwipeActionsConfiguration?
    open var trailingSwipeActionsConfiguration: UISwipeActionsConfiguration?
}

extension UICollectionViewListCell {
    // 默认配置内容
    public func defaultContentConfiguration() -> UIListContentConfiguration
}

extension UICollectionViewListCell {
    // 附件
    public var accessories: [UICellAccessory]
}

UICollectionViewListCell 所有东西都在这儿了,其继承自 UICollectionViewCell,而 UICollectionViewCell 并没有像UITableViewCell 一样会根据样式提供默认的组件,如imageView, label,下划线等。那 UICollectionViewListCell 我们这些非我们实现的组件,系统是在哪里实现的?

抛开几个缩进属性,滑动配置,附件集合这些用处明确的属性,只剩下一个 defaultContentConfiguration ,进而我们可以在其父类CollectionView 中发现 iOS14 新增的 contentConfiguration 属性,该属性遵循 UIContentConfiguration 协议,而这个协议总共就两个方法:

func makeContentView() -> UIView & UIContentView
func updated(for: UIConfigurationState) -> Self

makeContentView 应当就是是建立视图的方法,返回了一个符合 UIContentView 协议 UIView...系统创建的 label,imageView 等缺省组件配置就在这里面进行的。

这个 UIContentView 包含了一个符合 UIContentConfiguration 协议的属性 configuration。简单的说就是 makeContentView 返回了一个遵循 UIConfigurationsState 的视图本身。

updated 需要传入一个 UIConfigurationsState 类型,现在只需要知道它是一个状态管理配置器,下文将会专门介绍,这里暂时略过。



使用 UIContentConfiguration 进行配置

UIContentConfiguration 是一个协议,UICollectionViewListCell 生成的默认配置 UIListContentConfiguration 就遵循此协议。

UIListContentConfiguration 为 struct,值类型。官方说法是:轻量级,系统开销低,开发者不用再关注 Cell 的各种配置及状态管理,交给它就可以。不仅可以配合系统提供的组件使用,也可以自定义 UIContentConfiguration来适配自定义的Cell或item

系统提供的 UIListContentConfiguration 实现起来也非常简单:


let cellRegist = UICollectionView.CellRegistration<MyCell, Int> { (cell, indexPath, item) in
            
    // 配置 UIListContentConfiguration
    var config = cell.defaultContentConfiguration()
    config.text = "item \(indexPath.row)"
    config.textProperties.color = .red
    config.textProperties.alignment = .center
    config.secondaryText = "XXXXXXX"
    config.secondaryTextProperties.color = .blue
    config.secondaryTextProperties.alignment = .center
    cell.contentConfiguration = config

    // 设置选中色
    //··········

    // 右滑
    //··········
}

UIListContentConfiguration 包含了所有缺省组件的各种状态的设置方式,例如 title 的 textProperties。配置会在渲染前以及每次修改后调用,UITableView 中的 cell 也能用使用:

let cell:UITableViewCell = ........

//此处获取到的 content 为 UIListContentConfiguration
var content = cell.defaultContentConfiguration()   
content.text = "Title"
content.secondaryText = "1111"
cell.contentConfiguration = content
reutrn cell

Tips:需留意,苹果从 iOS14 开始不建议直接对 UICollectionViewListCell 或 UITableViewCell 系统生成的缺省组件操作,新增的 UICollectionViewListCell 直接就没有暴露缺省组件的属性以供访问。
推荐使用 UIListContentConfiguration 的方式,在未来直接对系统组件进行操作可能会被禁止,像UITableViewCell的缺省组件都被标警告:
Use UIListContentConfiguration instead, this property will be deprecated in a future release.



状态管理 UIConfigurationState

该协议提供了一个配置状态对象的蓝图,它包括一个特征集合以及所有影响视图外观的常见状态。目前的测试版中,有两种现成的 state : UICellConfigurationState 与 UIViewConfigurationState。

以 UICellConfigurationState 为例,其包含的状态如下:

var cellDragState: UICellConfigurationState.DragState
var cellDropState: UICellConfigurationState.DropState
var hashValue: Int
var isDisabled: Bool
var isEditing: Bool
var isExpanded: Bool
var isFocused: Bool                     
var isHighlighted: Bool                 
var isSelected: Bool                   
var isSwiped: Bool                     
var traitCollection: UITraitCollection   // 布局相关

包含了一个 Cell 的所有基本状态,也包含了 UIViewConfigurationState 的所有状态。

UIConfigurationState 配合 UIContentConfiguration 进行视图配置的状态管理,苹果推荐使用这种方式来分开管理视图的“状态”和“显示”。就像官方例子中一样:

private class ItemListCell: UICollectionViewListCell {
    private var item: Item? = nil
    
    func updateWithItem(_ newItem: Item) {
        guard item != newItem else { return }
        item = newItem
        setNeedsUpdateConfiguration()
    }
    
    //........
}

ItemListCell 是例子中其他 Cell 的基类。此处的 Item 是一个 遵循 Hashable 协议的 struct,可以将 item 理解为一个模型基类(BaseModel)。当 Cell 更新数据时,使用 setNeedsUpdateConfiguration() 来手动调用 updateConfiguration(using state:),在 updateConfiguration 方法中根据item(模型),根据state(ConfigurationState)Cell来进行各种Cell 视图设置。

使用 UIConfigurationStateCustomKey 添加自定义状态

对于自定义的视图,有些自定义的状态,UIConfigurationState 可以通过 UIConfigurationStateCustomKey 进行添加。官方示例写的要骚一点,这里就偷个懒:

// 再 UIConfigurationStateCustomKey 中声明一个自定义状态 isArchived 的 Key
extension UIConfigurationStateCustomKey {
    static let isArchived = UIConfigurationStateCustomKey("com.my-app.MyCell.isArchived")
}

// 在 UICellConfigurationState 扩展中为自定义状态提供实现
extension UICellConfigurationState {
    var isArchived: Bool {
        get { return self[.isArchived] as? Bool ?? false } 
        set { self[.isArchived] = newValue }
    }
}

class MyCell: UICollectionViewCell {
    var isArchived: Bool { 
        didSet {
            // 每次设置isArchived时都会调用更新
            if oldValue != isArchived { 
                setNeedsUpdateConfiguration()
            }
        }
    }

    override var configurationState: UICellConfigurationState {
        var state = super.configurationState
        state.isArchived = self.isArchived 
        return state
    }

    override func updateConfiguration(using state: UICellConfigurationState) {
        var backgroundConfig = UIBackgroundConfiguration.listPlainCell().updated(for: 
state)

        if state.isArchived {
            backgroundConfig.visualEffect = UIBlurEffect(style: .systemMaterial) 
        }

        self.backgroundConfiguration = backgroundConfig 
    }
}

实现自己基于 State 的方式,可以参考系统的 UICollectionViewListCell 。系统在其父类 UICollectionViewCell 进行了 Configuration 相关的扩展:

@available(iOS 14.0, tvOS 14.0, *)
@available(iOS 14.0, tvOS 14.0, *)
extension UICollectionViewCell {

    @available(iOS 14.0, tvOS 14.0, *)
    public var contentConfiguration: UIContentConfiguration?
}

@available(iOS 14.0, tvOS 14.0, *)
extension UICollectionViewCell {

    @available(iOS 14.0, tvOS 14.0, *)
    public var backgroundConfiguration: UIBackgroundConfiguration?
}
extension UICollectionViewCell {

    @available(iOS 14.0, tvOS 14.0, *)
    @objc(_bridgedConfigurationState) dynamic open var configurationState: UICellConfigurationState { get }

    @available(iOS 14.0, tvOS 14.0, *)
    @objc(_bridgedUpdateConfigurationUsingState:) dynamic open func updateConfiguration(using state: UICellConfigurationState)
}

UICollectionViewCell这里定义了与 ContentConfiguration 相关的属性与方法,这就意味着所有基于 UICollectionViewCell 的子类都无需从头实现 Configuration 与 State 相关部分。

尾语:
iOS14 中苹果对 CollectionView 的改动,再结合近两年苹果的发展的SwiftUI,全端开发,Combine 等等特性结合来看,新一代的苹果技术栈特点也越来越明朗了:便捷高效、可视化、更现代。对于 CollectionView 来说,经过 iOS13 一年的锤炼,现在开始尝试新版的 CollectionView 最适合不过了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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