UICollectionView 的新进展
Advances in UICollectionView
2020年6月26日
一句话判断
UICollectionView 在 iOS 14 中迎来了它历史上最大的 API 更新:Compositional Layout 支持列表布局、Diffable Data Source 支持重新排序、Cell Registration 大幅简化,这些变化让你几乎可以扔掉所有的 UICollectionView 扩展库。
这场 Session 讲了什么
iOS 13 引入了 Compositional Layout 和 Diffable Data Source,但它们还有一些功能缺失。iOS 14 补齐了这些短板。Session 重点介绍了四项更新。
第一,Compositional Layout 现在支持列表布局(List Layout),你可以用 UICollectionLayoutListConfiguration 创建外观和行为都与 UITableView 一致的列表,但拥有 UICollectionView 的全部灵活性。第二,Diffable Data Source 支持拖拽重新排序,通过 canReorderItem 和 reorderItem 回调让用户拖动 Cell 来重新排列数据。第三,全新的 Cell Registration API(UICollectionView.CellRegistration)替代了繁琐的 dequeueReusableCell + 可选类型转换的模式。第四,UICollectionView 现在支持 Section Snapshots,可以为不同的 Section 使用完全不同的数据源和布局。
值得深挖的点
列表布局:UITableView 的终结者
UICollectionLayoutListConfiguration 可以创建外观与 UITableView 完全一致的列表,包括滑动删除、层级缩进、 Swipe Actions 等。但它比 UITableView 灵活得多——同一个 CollectionView 中,某些 Section 用列表布局,另一些用网格布局,只需在 Section 级别指定不同的布局即可。Apple 明确表示这是 UITableView 的推荐替代方案。
Section Snapshot:多 Section 数据管理
NSDiffableDataSourceSectionSnapshot 让你可以独立管理每个 Section 的数据。比如一个设置页面有”通用”和”隐私”两个 Section,每个 Section 有自己的增删改逻辑。使用 Section Snapshot,你只需要更新变化的那个 Section,其他 Section 不受影响。这比刷新整个 Snapshot 效率更高。
代码片段
创建列表布局的 CollectionView
场景:用 Compositional Layout 创建一个设置页面。
import UIKit
class SettingsViewController: UIViewController {
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, SettingItem>!
enum Section: CaseIterable {
case general
case privacy
}
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView()
setupDataSource()
applyInitialSnapshot()
}
private func setupCollectionView() {
// 创建列表布局配置
let listConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
listConfig.headerMode = .firstItemInSection // 第一个 Item 作为 Header
// 允许滑动删除
// listConfig.trailingSwipeActionsConfigurationProvider = { indexPath in ... }
let layout = UICollectionViewCompositionalLayout.list(using: listConfig)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
view.addSubview(collectionView)
}
private func setupDataSource() {
// 使用新的 CellRegistration API
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, SettingItem> {
(cell, indexPath, item) in
// 配置列表 Cell 的内容
var content = cell.defaultContentConfiguration()
content.text = item.title
content.secondaryText = item.subtitle
content.image = UIImage(systemName: item.iconName)
cell.contentConfiguration = content
// 添加附件视图(如 disclosure indicator)
cell.accessories = [.disclosureIndicator()]
}
// 创建 Diffable Data Source
dataSource = UICollectionViewDiffableDataSource<Section, SettingItem>(
collectionView: collectionView
) { collectionView, indexPath, item in
collectionView.dequeueConfiguredReusableCell(
using: cellRegistration,
for: indexPath,
item: item
)
}
}
private func applyInitialSnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<Section, SettingItem>()
snapshot.appendSections([.general, .privacy])
snapshot.appendItems([
SettingItem(title: "通知", subtitle: "管理通知设置", iconName: "bell"),
SettingItem(title: "显示与亮度", subtitle: "调整屏幕亮度", iconName: "sun.max")
], toSection: .general)
snapshot.appendItems([
SettingItem(title: "定位服务", subtitle: "管理 App 定位权限", iconName: "location"),
SettingItem(title: "隐私", subtitle: "查看数据使用情况", iconName: "hand.raised")
], toSection: .privacy)
dataSource.apply(snapshot, animatingDifferences: true)
}
}
实现拖拽重新排序
场景:让用户通过拖拽调整列表中项目的顺序。
extension SettingsViewController {
func setupReorderingSupport() {
// 让 Data Source 支持重新排序
dataSource.reorderingHandlers.canReorderItem = { item in
// 只有特定类型的项目可以被重新排序
return item.isReorderable
}
dataSource.reorderingHandlers.didReorder = { transaction in
// 用户完成拖拽后,更新底层数据
let updatedItems = transaction.finalSnapshot.itemIdentifiers
updateItemOrder(updatedItems)
}
}
private func updateItemOrder(_ items: [SettingItem]) {
// 将新顺序持久化到本地存储或服务器
var order = 0
for item in items {
var mutableItem = item
mutableItem.sortOrder = order
order += 1
}
// 保存更新后的顺序
SettingStorage.shared.saveOrder(items)
}
}
使用 Section Snapshot 独立管理每个 Section
场景:分别更新不同 Section 的数据。
func updateGeneralSection() {
// 只更新 general Section,不影响其他 Section
var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<SettingItem>()
let items = [
SettingItem(title: "通知", subtitle: "管理通知", iconName: "bell"),
SettingItem(title: "声音", subtitle: "铃声与振动", iconName: "speaker.wave.2"),
SettingItem(title: "专注模式", subtitle: "勿扰模式", iconName: "moon")
]
sectionSnapshot.append(items)
// 应用到指定 Section
dataSource.apply(sectionSnapshot, to: .general, animatingDifferences: true)
}
func updatePrivacySection() {
// 独立更新 privacy Section
var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<SettingItem>()
let items = [
SettingItem(title: "定位服务", subtitle: "管理权限", iconName: "location"),
SettingItem(title: "通讯录", subtitle: "管理通讯录访问", iconName: "person.crop.circle")
]
sectionSnapshot.append(items)
dataSource.apply(sectionSnapshot, to: .privacy, animatingDifferences: true)
}
最佳实践
已有项目:如果你还在使用 UITableView,计划迁移到 UICollectionView + List Layout。迁移路径是平滑的——List Layout 的外观与 UITableView 一致,你的用户不会察觉到变化。但你会获得 Compositional Layout 的灵活性,未来想混用网格和列表布局时不需要重写。优先使用 CellRegistration 替换旧的 dequeueReusableCell 模式。
新项目:直接使用 UICollectionView + Compositional Layout + Diffable Data Source。不要再用 UITableView。即使当前只需要简单的列表,未来如果需要添加网格、轮播等布局,你的架构已经准备好了。
还有什么值得关注
UICollectionLayoutListConfiguration支持三种外观:.plain、.grouped、.insetGrouped、.sidebar。- Swipe Actions 现在可以在列表布局中原生支持,不需要自定义手势识别器。
- iOS 14 的 iPadOS 适配中,UICollectionView 的多列布局和拖拽支持更加完善。