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 最适合不过了。