UICollectionViewDiffableDataSource及NSDiffableDataSourceSnapshot使用介绍

 1. 前言

在iOS13出来后,苹果对UICollectionView的API进行了更新,推出了UICollectionViewDiffableDataSource协议,下面就一起来看一下这个协议。

2. UICollectionViewDiffableDataSource使用

在使用之前,先看一下它的定义。

@MainActor class UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> : NSObject where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable

DiffableDataSource对象是一种与UICollectionView对象协同工作的特殊数据源类型。它提供了以简单、有效的方式管理UICollectionView数据和UI更新所需的行为。它还符合UICollectionViewDataSource协议,并为协议的所有方法提供实现。

那么他们是如何工作的呢?主要分为4步:

  1. 将dataSource与CollectionView进行关联。
  2. 实现一个Cell的闭包方法来配置CollectionView的Cell。
  3. 生成当前要显示的数据。
  4. 将数据显示在UI上。

在创建UICollectionViewDiffableDataSource对象的时候,我们需要传入一个collectionview对象,这个collectionview对象则会和diffableDataSource对象关联。同时还要传入一个闭包(CellProvider),来配置我们要显示的每一个cell。

先看一下初始化方法:

public init(collectionView: UICollectionView, cellProvider: @escaping UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>.CellProvider)

public typealias CellProvider = (_ collectionView: UICollectionView, _ indexPath: IndexPath, _ itemIdentifier: ItemIdentifierType) -> UICollectionViewCell?

方法具体实现如下:

// 创建dataSource,并配置CellProvider。
dataSource = UICollectionViewDiffableDataSource<Int, UUID>(collectionView: collectionView) {
    (collectionView: UICollectionView, indexPath: IndexPath, itemIdentifier: UUID) -> UICollectionViewCell? in
    // Configure and return cell.
}

当然啦,上面的泛型我们需要根据实际项目中用到的类型而定。

当创建好了diffableDataSource,配置好了实现Cell的闭包,接下来就要展示数据了,这时就要用到一个叫快照的东西(NSDiffableDataSourceSnapshot),App中要显示的数据基本上都会随着时间而变化,比如要展示视频的界面,而在某一个时间点上的数据,可称为数据的当前状态State,NSDiffableDataSourceSnapshot就是捕获当前的数据状态,并在UI上显示。下一节将会对NSDiffableDataSourceSnapshot进行进一步说明。

目前先把上面四步中的后两步代码贴出来,免得不完整。

// 创建一个snapshot.
var snapshot = NSDiffableDataSourceSnapshot<Int, UUID>()        

// 给snapshot添加当前的数据.
snapshot.appendSections([0])
snapshot.appendItems([UUID(), UUID(), UUID()])

// 调用dataSource的apply方法显示数据
dataSource.apply(snapshot, animatingDifferences: true)

经过上面的几步,就能将数据显示出来,成功的用UICollectionViewDiffableDataSource替换掉了UICollectionViewDataSource协议,不需要再实现UICollectionViewDataSource协议中的有几个section,每个section有几个item,以及cellforitem的方法。

UICollectionViewDiffableDataSource常用方法:

1. UICollectionViewDiffableDataSource初始化方法,在初始化的时候需要传入一个UICollectionView对象与之绑定,同事还带一个CellProvider闭包,在这个闭包里面配置cell。

init(collectionView: UICollectionView, cellProvider: UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>.CellProvider)

2. CellProvider闭包,用来配置cell,相当于UICollectionViewDataSource里面的cellForItem方法。

typealias UICollectionViewDiffableDataSource.CellProvider

// 定义如下:
typealias UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>.CellProvider = (_ collectionView: UICollectionView, _ indexPath: IndexPath, _ itemIdentifier: ItemIdentifierType) -> UICollectionViewCell?

该闭包带有三个参数,collectionView、indexPath以及itemIdentifier,并要求返回一个cell。

示例如下:

let cellRegistration = UICollectionView.CellRegistration<TextCell, Int> { (cell, indexPath, identifier) in
    // Populate the cell with our item description.
    cell.label.text = "\(identifier)"
    cell.contentView.backgroundColor = .cornflowerBlue
    cell.label.textAlignment = .center
    cell.label.font = UIFont.preferredFont(forTextStyle: .title1)
}
        
dataSource = UICollectionViewDiffableDataSource<Section, Int>(collectionView: collectionView) { (collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in
    // Return the cell.
    return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier)
}

3. 设置Section header和Footer的supplementaryViewProvider闭包方法

typealias UICollectionViewDiffableDataSource.SupplementaryViewProvider

var supplementaryViewProvider: UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>.SupplementaryViewProvider?

SupplementaryViewProvider定义如下:

typealias UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>.SupplementaryViewProvider = (_ collectionView: UICollectionView, _ elementKind: String, _ indexPath: IndexPath) -> UICollectionReusableView?

该闭包带有三个参数,collectionView、kind以及indexPath,并要求返回一个UICollectionReusableView。kind就是给header或者footer的一个标识。

看一下示例代码:

let headerRegistration = UICollectionView.SupplementaryRegistration<TitleSupplementaryView>(elementKind: "SectionHeader") { (supplementaryView, string, indexPath) in
    supplementaryView.label.text = "\(string) for section \(indexPath.section)"
    supplementaryView.backgroundColor = .lightGray
    supplementaryView.layer.borderColor = UIColor.black.cgColor
    supplementaryView.layer.borderWidth = 1.0
}
        
let footerRegistration = UICollectionView.SupplementaryRegistration<TitleSupplementaryView>(elementKind: "SectionFooter") { (supplementaryView, string, indexPath) in
    supplementaryView.label.text = "\(string) for section \(indexPath.section)"
    supplementaryView.backgroundColor = .lightGray
    supplementaryView.layer.borderColor = UIColor.black.cgColor
    supplementaryView.layer.borderWidth = 1.0
}

dataSource.supplementaryViewProvider = { (view, kind, index) in
    return self.collectionView.dequeueConfiguredReusableSupplementary(using: kind == "SectionHeader" ? headerRegistration : footerRegistration, for: index)
}

4. 根据indexPath返回指定的Item对象。

func itemIdentifier(for: IndexPath) -> ItemIdentifierType?

5. 根据Item对象返回其indexPath.

func indexPath(for: ItemIdentifierType) -> IndexPath?

6. 返回指定索引位置的Section。

func sectionIdentifier(for: Int) -> SectionIdentifierType?

7. 返回Section所在的索引位置Int.

func index(for: SectionIdentifierType) -> Int?

8. 获取当前CollectionView展示的snapshot数据。

func snapshot() -> NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType>

9. 用当前的snapshot更新UI。

// 更新当前的snapshot到Collectionview上,并决定是否带动画。
func apply(NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType>, animatingDifferences: Bool)

// 更新当前的snapshot到Collectionview上, 更新完成后会有完成回调。
func apply(NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType>, animatingDifferences: Bool, completion: (() -> Void)?)

10. 获取指定Section所展示的items。

func snapshot(for: SectionIdentifierType) -> NSDiffableDataSourceSectionSnapshot<ItemIdentifierType>

11. 更新指定Section的items。

// 更新指定Section的items,并决定是否有动画,以及更新完成回调。
func apply(NSDiffableDataSourceSectionSnapshot<ItemIdentifierType>, to: SectionIdentifierType, animatingDifferences: Bool, completion: (() -> Void)?)

// 更新指定Section的items,并决定是否有动画。
func apply(NSDiffableDataSourceSectionSnapshot<ItemIdentifierType>, to: SectionIdentifierType, animatingDifferences: Bool)

3. NSDiffableDataSourceSnapshot使用

 A representation of the state of the data in a view at a specific point in time.

也就是说NSDiffableDataSourceSnapshot用于展示数据在某一时间的状态。

struct NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType> where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable

从定义上看,NSDiffableDataSourceSnapshot由Sections和items组成,并且可以按照我们想要的顺序进行显示,我们也可以通过增加、删除和移动section和item来配置显示的内容。

特别要注意的是:每一个section和item都需要遵守Hashable协议,必须有一个唯一标识符,可以用Swift值类型中的struct和enum,包括一些内置的类型,比如:Int、String、UUID等,如果用Swift Class作为标识符,那么该Class必须为NSObject的子类。

就上面代码中写的一样,用NSDiffableDataSourceSnapshot显示数据,第一步创建NSDiffableDataSourceSnapshot对象,并添加数据,第二步,dataSource调用apply方法显示数据。

除了直接创建一个空的Snapshot,也可以通过dataSource的snapshot()方法获取当前的snapshot对象,然后修改数据,最后apply显示。

下面看一下NSDiffableDataSourceSnapshot相关的方法:

NSDiffableDataSourceSnapshot常用方法:

1. 初始化方法,创建一个空的Snapshot:

init()

2. 添加具有唯一标识符的Section数据,需要传一个Section数组:

func appendSections([SectionIdentifierType])

3. 向具有唯一标识符的Section添加具有唯一标识符的Item数据,如果只有一个Section,toSection参数可以不传,如果是多Section数据结构,遍历Sections数据进行添加Items则可。

func appendItems([ItemIdentifierType], toSection: SectionIdentifierType?)

4. 获取当前snapshot下Item数量

var numberOfItems: Int

5. 获取当前snapshot下Section数量

var numberOfSections: Int

6. 获取当前snapshot下指定Section的Item数量

func numberOfItems(inSection: SectionIdentifierType) -> Int

7. 获取当前snapshot下所有Item对象数组

var itemIdentifiers: [ItemIdentifierType]

8. 获取当前snapshot下所有Section对象数组

var sectionIdentifiers: [SectionIdentifierType]

9. 在当前snapshot下,根据指定Item,获取该Item的索引。

func indexOfItem(ItemIdentifierType) -> Int?

10. 在当前snapshot下,根据指定Section,获取该Section的索引。

func indexOfSection(SectionIdentifierType) -> Int?

11. 在当前snapshot下,根据指定Section,获取其下的所有Items。

func itemIdentifiers(inSection: SectionIdentifierType) -> [ItemIdentifierType]

12. 在当前snapshot下,获取指定Item所在的Section。

func sectionIdentifier(containingItem: ItemIdentifierType) -> SectionIdentifierType?

3.2 插入Sections和Items方法:

1. 在指定Item后面插入Items数组

func insertItems([ItemIdentifierType], afterItem: ItemIdentifierType)

2. 在指定Item前面插入Items数组

func insertItems([ItemIdentifierType], beforeItem: ItemIdentifierType)

3. 在指定的Section后面插入Section数组

func insertSections([SectionIdentifierType], afterSection: SectionIdentifierType)

4. 在指定的Section前面插入Section数组

func insertSections([SectionIdentifierType], beforeSection: SectionIdentifierType)

3.3 移除Sections和Items方法

1. 从当前snapshot中移除所有Items

func insertSections([SectionIdentifierType], beforeSection: SectionIdentifierType)

2. 从当前snapshot中移除指定的Items

func deleteItems([ItemIdentifierType])

3. 从当前的snapshot中移除指定的Sections

func deleteSections([SectionIdentifierType])

3.4 重新将Sections和Items排序方法

1. 将指定的Item从当前位置移动到某个Item的后面位置

func moveItem(ItemIdentifierType, afterItem: ItemIdentifierType)

2. 将指定的Item从当前位置移动到某个Item的前面位置

func moveItem(ItemIdentifierType, beforeItem: ItemIdentifierType)

3. 将指定的Section从当前位置移动到某个Section的后面位置

func moveSection(SectionIdentifierType, afterSection: SectionIdentifierType)

4. 将指定的Section从当前位置移动到某个Section的前面位置

func moveSection(SectionIdentifierType, beforeSection: SectionIdentifierType)

3.5 刷新方法

1. 重新加载snapshot中指定Items。

func reloadItems([ItemIdentifierType])

2. 更新当前snapshot中的指定Items,并保留已经存在的cell。

func reconfigureItems([ItemIdentifierType])

该方法在iOS15、iPadOS15、tvOS15、Xcode13及以后才有的方法,与reloadItems(_:)方法不同的是,这个方法保留已有cell,更新cell显示内容,如果追求更好的性能,那么使用reconfigureItems(_:)更好一些。

CellProvider闭包必须为所提供的indexPath dequeue相同类型的cell,并且必须为给定的indexPath返回相同的现有cell。因为reconfigureItems(_:)这个方法会重新配置现有的cell,所以UICollectionView不会对每个dequeue的cell调用prepareForReuse。如果为indexPath返回不同类型的cell,请使用reloadItems(_:)代替。

个人觉得通俗点就是同一类型的cell换数据,用reconfigureItems(_:)方法,如果换不同数据类型的数据,且cell样式也不一样了,那么就用reloadItems(_:)

3. 返回snapshot更新后,重新配置的Items。(iOS15+、iPadOS15+、tvOS15+、Xcode13+)

var reconfiguredItemIdentifiers: [ItemIdentifierType]

4. 返回snapshot更新后,已更新的Items。(iOS15+、iPadOS15+、tvOS15+、Xcode13+)

var reloadedItemIdentifiers: [ItemIdentifierType]

5. 更新指定Section的数据

func reloadSections([SectionIdentifierType])

6. 返回snapshot更新后,已更新的Sections。(iOS15+、iPadOS15+、tvOS15+、Xcode13+)

var reloadedSectionIdentifiers: [SectionIdentifierType]

总结

本篇文章主要介绍了如何使用UICollectionViewDiffableDataSource和NSDiffableDataSourceSnapshot去代替UICollectionViewDataSource完成UICollectionView的数据方面的填充。

文章比较简单,且有很多同仁已经发过类似的博客,不过好记性不如烂笔头,记录下来,自己学习了,也希望帮到他人。

如果你喜欢就点个赞或者关注一波噢!!!

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值