Advances in UICollectionView
Swift & UI 进阶 32m

UICollectionView 的新进展

Advances in UICollectionView

2020年6月26日

在 Apple 官方观看视频

一句话判断

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 支持拖拽重新排序,通过 canReorderItemreorderItem 回调让用户拖动 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 的多列布局和拖拽支持更加完善。
WWDC 2020