WWDC20 10045 & 10026 - Data Source 和 UICollectionView 在 iOS14 中的新特性

概述

去年苹果给 UICollectionView 中引入了 Diffable Data Source 和 UICollectionViewCompositionalLayout 两个一个数据和一个 UI 的全新的 API(可以参考去年的两篇文章 UICollectionView 全新布局框架:UICollectionViewCompositionalLayoutData Source 新特性:基于 Diffable 实现局部刷新 ),在今年苹果对这两个API 进行了更多的更新,使得 UICollectionView 使用起来更加方便快捷。本文将从数据和 UI 两个方面来讨论这次苹果在 UICollectionView 方面的更新。分别对应着Advances in diffable data sources - 10045Lists in UICollectionView - 10026 两个 session 。

Diffable Data Source

前言

在 iOS 13 中,我们引入了 Snapshots 这个数据类型,来使我们更方便的去管理 UICollectionView 中 UI 的状态。当我们需要更新 UICollectionView 时,只需要新建一个 snapshot ,而 Diffable Data Source 会计算出差异,并且自动执行动画,不需要任何其他操作。而在 iOS 14 中,苹果在 Diffable Data Source 的基础上又加入了两个新的功能,一个是 Section Snapshots 和 对数据进行排序的支持。

Section Snapshots

SectionSnapshots 可以对 UICollectionView 按照 Section 进行数据的更新。在 emoji explore 中,每一个部分都可以作为一个 SectionSnapshots。

public struct NSDiffableDataSourceSectionSnapshot<ItemIdentifierType> where ItemIdentifierType : Hashable {

    public init()

    public init(_ snapshot: NSDiffableDataSourceSectionSnapshot<ItemIdentifierType>)

    public mutating func append(_ items: [ItemIdentifierType], to parent: ItemIdentifierType? = nil)

    public mutating func insert(_ items: [ItemIdentifierType], before item: ItemIdentifierType)

    public mutating func insert(_ items: [ItemIdentifierType], after item: ItemIdentifierType)

    public mutating func delete(_ items: [ItemIdentifierType])

    public mutating func deleteAll()

    public mutating func expand(_ items: [ItemIdentifierType])

    public mutating func collapse(_ items: [ItemIdentifierType])

    public mutating func replace(childrenOf parent: ItemIdentifierType, using snapshot: NSDiffableDataSourceSectionSnapshot<ItemIdentifierType>)

    public mutating func insert(_ snapshot: NSDiffableDataSourceSectionSnapshot<ItemIdentifierType>, before item: (ItemIdentifierType))

    public mutating func insert(_ snapshot: NSDiffableDataSourceSectionSnapshot<ItemIdentifierType>, after item: (ItemIdentifierType))

    public func isExpanded(_ item: ItemIdentifierType) -> Bool

    public func isVisible(_ item: ItemIdentifierType) -> Bool

    public func contains(_ item: ItemIdentifierType) -> Bool

    public func level(of item: ItemIdentifierType) -> Int

    public func index(of item: ItemIdentifierType) -> Int?

    public func parent(of child: ItemIdentifierType) -> ItemIdentifierType?

    public func snapshot(of parent: ItemIdentifierType, includingParent: Bool = false) -> NSDiffableDataSourceSectionSnapshot<ItemIdentifierType>

    public var items: [ItemIdentifierType] { get }

    public var rootItems: [ItemIdentifierType] { get }

    public var visibleItems: [ItemIdentifierType] { get }
}

在 iOS 13 中,我们介绍了 NSDiffableDataSourceSnapshot 这个类,与之类似的,在 NSDiffableDataSourceSectionSnapshot 同样的又 append、delete、move、insert 来对每个 section 的数据进行更新。在 iOS 14苹果新增了 expand, collapse这个展开收起的更新的方法,而 parent 和 rootItems 可以对 items设置他的父 items。
我们来看看如何去使用 SectionSnapshots :
我们还是用上面那个 emoji explore 作为例子,我们可以先用 iOS 13中的 NSDiffableDataSourceSnapshot 创建好每个 section ,然后通过 NSDiffableDataSourceSectionSnapshot 来更新每个 section 当中的 item。

   let sections: [Section] = [.recent, .top, .suggested]
   var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
   snapshot.appendSections(sections)
   dataSource.apply(snapshot, animatingDifferences: animated)

   // update each section's data via section snapshots in the existing position
   for section in sections {
      let sectionItems = items(for: section)
      var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
      sectionSnapshot.append(sectionItems)
      dataSource.apply(sectionSnapshot, to: section, animatingDifferences:animated)
   }

同时在 emoji explore 这个中,第二个部分我们可以看到每个 item 又有子的关系,我们可以通过 NSDiffableDataSourceSectionSnapshot 来添加第二级的子 item。

sectionSnapshot.append(["Smileys", "Nature", 
                        "Food", "Activities",
                        "Travel", "Objects", "Symbols"])
sectionSnapshot.append(["🥃", "🍎", "🍑"], to: "Food")

在 iOS14 的 NSDiffableDataSourceSectionSnapshot 新加入了expand,collapse 这两个 snapshot 可以是我们方便的对 section 中的 item 进行展开收起操作。通过这个 snapshot ,UICollectionView 自动的对相应的 sectinon 执行操作,并展示相应的动画。

let items = Array(0..<3).map { Item(title: "Item \($0)") }
sectionSnapshot.append(items, to: headerItem)
sectionSnapshot.expand([headerItem])
dataSource.apply(sectionSnapshot, to: section)

我们也可以通过 snapshot 的回调拿到每个 section 所展开收起的状态,并且可以加以控制。

  struct SectionSnapshotHandlers<Item> {
    var shouldExpandItem: ((Item) -> Bool)?
    var willExpandItem: ((Item) -> Void)?

    var shouldCollapseItem: ((Item) -> Bool)?
    var willCollapseItem: ((Item) -> Void)?

    var snapshotForExpandingParent: ((Item, NSDiffableDataSourceSectionSnapshot<Item>) -> NSDiffableDataSourceSectionSnapshot<Item>)?
  }

Reordering Support

在 iOS14 中加入了新的API,Reordering Support 和 UITableView 类似,提供了方法给我们更加方便的对 UICollectionView 中的 item 进行排序操作。

extension UICollectionViewDiffableDataSource {

  struct ReorderingHandlers {
    var canReorderItem: ((Item) -> Bool)?
    var willReorder: ((NSDiffableDataSourceTransaction<Section, Item>) -> Void)?
    var didReorder: ((NSDiffableDataSourceTransaction<Section, Item>) -> Void)?
  }

  var reorderingHandlers: ReorderingHandlers
}

canReorderItem 和 didReorder 分别对应着 tableview 中 canMoveRowAtIndexPath 和 moveRowAtIndexPath 这两个方法,通过实现reorderingHandlers中的这两个方法,我们可以像 UITableView 一样,对 UICollectionView 中的 item 进行排序。

```
struct NSDiffableDataSourceTransaction {
var initialSnapshot: NSDiffableDataSourceSnapshot { get }
var finalSnapshot: NSDiffableDataSourceSnapshot { get }
var difference: CollectionDifference { get }
var sectionTransactions: [NSDiffableDataSourceSectionTransaction] { get }
}
struct NSDiffableDataSourceSectionTransaction {
var sectionIdentifier: Section { get }
var initialSnapshot: NSDiffableDataSourceSectionSnapshot { get }
var finalSnapshot: NSDiffableDataSourceSectionSnapshot { get }
var difference: CollectionDifference { get }
}

top Created with Sketch.