Advances in diffable data sources

Written by DasAuto

Description: Diffable data sources dramatically simplify the work involved in managing and updating collection and table views to create dynamic and responsive experiences in your apps. Discover how you can use section snapshots to efficiently build lists and outline collection views for iOS and iPadOS and provide support for implementing the sidebar in an iPad app. We’ll also show you how to simplify cell reordering using UICollectionViewDiffableDataSource to help you streamline your code and build app interfaces more quickly. This session builds on 2019’s “Advances in UI Data Sources,” which you may want to check out first.

Sample Code

Download the sample code to learn more about Modern Collection Views.

Diffable Data Source

Recap

Section Snapshots

  • Encapsulate the data for a single section in a UICollectionView.
  • Allow data sources to be more composable into section-sized chunks of data.
  • Can present the same data with different layout in different sections.
  • Allow modeling of hierarchical data, which is needed to support rendering outline-style UIs.

Examples

  • Top: Horizontally scrolling section.
  • Middle: Outline style section.
  • Bottom: List section.

It composes Diffable Data Source from three distinct section snapshots, each representing a single section's content.

APIs

  • Use append(:parent:) API to add content into a section snapshot.
  • The optional parent parameter allows us to create parent-child relationships in the section snapshot.
// UICollectionViewDiffableDataSource additions for iOS 14

extension UICollectionViewDiffableDataSource<Item, Section> {

    func apply(_ snapshot: NSDiffableDataSourceSectionSnapshot<Item>, 
               to section: Section, 
               animatingDifferences: Bool = true, 
               completion: (() -> Void)? = nil)

    func snapshot(for section: Section) ->   
                  NSDiffableDataSourceSectionSnapshot<Item>
}

Code example:

// Example of using snapshots and section snapshots together

func update(animated: Bool=true) {

   // Add our sections in a specific order
   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)
   }
}

Child snapshots

let childSnapshot = sectionSnapshot.snapshot(for: parent, includingParent: false)

Expanding and Collapsing Items

  • Expansion state is persisted.
  • Expansion state is managed as part of a section snapshot's state.
  • Call apply(_:to) to commit changes.
  • Automatic animations.
struct NSDiffableDataSourceSectionSnapshot<Item: Hashable> {
   func expand(_ items: [Item])
   func collapse(_ items: [Item])
   func isExpanded(_ item: Item) -> Bool
}

Section snapshot handlers

To be notified about expansion state changes caused by these user interactions.

// Section Snapshot Handlers: handling user interactions for expand / collapse state changes

extension UICollectionViewDiffableDataSource {

  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>)?
  }
  
  var sectionSnapshotHandlers: SectionSnapshotHandlers<Item>
 
}
  • SectionSnapshotHandlers is a generic struct which contains five optional closures.
  • snapshotForExpandingParent is used for lazy loading when the content is expensive.

Reordering Support

  • Automatic snapshot updates.
  • Transactions.

Reordering Handlers

  • Used to be notified when a user-initiated reordering interaction took place.
  • Once notified, it can persist the new visual order to the application's backing store, which is its final source of truth.
// Diffable Data Source Reordering Handlers

extension UICollectionViewDiffableDataSource {

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

  var reorderingHandlers: ReorderingHandlers
}
  • Automatic reordering.
  • When the user is done with the reordering interaction, the didReorder closure is called.
  • Must provide both the canReorderItem and didReorder closure to enable the reordering feature.

Reordering Transactions

A transaction that describes the changes after reordering the items in the view.

struct NSDiffableDataSourceTransaction<Section, Item> {
   var initialSnapshot: NSDiffableDataSourceSnapshot<Section, Item> { get }
   var finalSnapshot: NSDiffableDataSourceSnapshot<Section, Item> { get }
   var difference: CollectionDifference<Item> { get }
   var sectionTransactions: [NSDiffableDataSourceSectionTransaction<Section, Item>] { get }
}
  • initialSnapshot is the state of the Diffable Data Source before the update is applied.
  • finalSnapshot is the state of the Diffable Data Source after the update is applied.
  • Apply the data directly to difference, if use a data type such as Array for the source of truth.
  • sectionTransactions provides per-section details about all the sections involved in this reordering update.

Section Transactions

struct NSDiffableDataSourceSectionTransaction<Section, Item> {
   var sectionIdentifier: Section { get }
   var initialSnapshot: NSDiffableDataSourceSectionSnapshot<Item> { get }
   var finalSnapshot: NSDiffableDataSourceSectionSnapshot<Item> { get }
   var difference: CollectionDifference<Item> { get }
}
  • sectionIdentifier for inspecting which section this sectionTransaction has been applied to.

Example

dataSource.reorderingHandlers.didReorder = { [weak self] transaction in 
   guard let self = self else { return }

   if let updateBackingStore = self.backingStore.applying(transaction.difference) {
      self.backingStore = updatedBackingStore
   }
}

Missing anything? Corrections? Contributions are welcome 😃

Related

Written by

DasAuto

DasAuto

💻 Programmer | 🕹 Lifetime Gamer