Is this the most simple, effective way to construct a diffable data source?

Adam Wareing
5 min readAug 13, 2023
iPhone with animations — credit: https://buildfire.com/

Introduction

We are going to explore a way you can construct datasource’s for your view using result builds in a very simple manor. This leads to a very testable design, that requires minimal code. Let’s get started.

In this example we are going to use a UICollectionView that uses a NSDiffableDataSourceSnapshot to drive the datasource.

Let’s begin

First we create two enums. One to represent each sections, and one to represent an item (or cell) inside the collection view with it’s associated value, being that cells model.

enum HomeViewSection {
case banner
case forYou
case suggested
}

enum HomeViewItem: Hashable {
case banner(String)
case videoContent(URL)
case imageContent(UIImage)
}

This next step requires adding a single file to your app which you can download from here.

In this example, a presenter is driving the UI, so we first create a type defined in the file mentioned above with the two generic types being the section and item enums we defined above.

private let viewModel = ViewModel<HomeViewSection, HomeViewItem>()

Now the fun begins.

Let’s create our UI. We make a call to the view model to build our UI using a result builder. We can give it a section, and inside of it put items, remember, these are the enums we defined at the beginning. This is the basis for defining the collection view diffable data source.

viewModel.build {
viewModel.section(.banner) {
HomeViewItem.banner("This is a banner")
}
}

We can also use for operators inside of this too. This is fantastic for mapping DTO’s or models received from an API or Database, to a format that the cell expects.

viewModel.build {
viewModel.section(.suggested) {
for content in homeContent {
HomeViewItem.imageContent(content)
}
}
}

And all we now need to do is tell our view to update the collection view by passing it the view model.

view?.apply(viewModel: viewModel)

That’s enough of our presenter for now. Let’s head over to our view controller to implement the collection view, and apply() function.

We will first create a collection view followed by a data source that is set on it. Remember to register your cells here too.

lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout())
lazy var dataSource: UICollectionViewDiffableDataSource<HomeViewSection, HomeViewItem> = createDataSource()

override func viewDidLoad() {
collectionView.dataSource = dataSource
}

Now we need to implement the function that creates the data source. Because our item identifier is an enum, we can simply switch the enum cases and create a cell, pass it a model, and return it to the source to show.

private func createDataSource() -> UICollectionViewDiffableDataSource<HomeViewSection, HomeViewItem> {
.init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
switch itemIdentifier {
case .banner(let string):
let cell: BannerCell = collectionView.dequeueCell(for: indexPath)
return cell.setup(string)

case .imageContent(let image):
let cell: ImageCell = collectionView.dequeueCell(for: indexPath)
return cell.setup(image)

case .videoContent(let video):
let cell: VideoCell = collectionView.dequeueCell(for: indexPath)
return cell.setup(video)
}
}
}

And finally we can implement the function that takes in this view model from the presenter and applies it to the collection view. By calling a generic function provided for you, snapshot(), it will turn the view model into a UIDiffableDataSource.

func apply(viewModel: ViewModel<HomeViewSection, HomeViewItem>) {
dataSource.apply(viewModel.snapshot(), animatingDifferences: true)
}

That is all the code you need to drive the entire data source of a collection view!

Testability

Of course, this wouldn’t be complete without a simple, clean solution for testing that your view models are correct.

Ideally, you would create a presenter that has some known inputs from loaders or repositories. We can replace our view controller with a mocked view that calls a function when applying the view model and check its what we expect.

By calling a generic function debugString() that automatically turns the view model into a string representation which we can assert equality with a hardcoded string which describes the sections and items inside of it.

let expectedViewModelString = "banner section: [Test.HomeViewItem.banner(\"This is a banner\")]suggested section: [Test.HomeViewItem.imageContent(\"Item 1\"), Test.HomeViewItem.imageContent(\"Item 2\")]"

view.applyViewModel = { viewModel in
XCTAssertEqual(viewModel.debugString(), expectedViewModelString)
expectedViewModel.fulfill()
}

This is fantastic for when the content your presenter loads can vary or include various amounts of sections where you can easily check to be that it is what you expect.

Extra for experts: Behind the scenes

Please note, this is just an explanation of how the file attached above works, and what it does. You do not need to implement this manually or even understand how it works but for those who are interested, carry on and let’s look how the magic happens behind the scenes.

First, it defines a section to represent the section and it’s items

struct SectionDTO<Section, Item> {
var section: Section
var content: [Item] = []
}

We have the generic view model that provides the functions for building the data source, adding a section, and generating the DiffableDataSourceSnapshots and debugStrings

class ViewModel<SectionIdentifierType: Hashable, ItemIdentifierType: Hashable> {

typealias Section = SectionDTO<SectionIdentifierType, ItemIdentifierType>
var sections: [Section] = []

func build(@ViewModelBuilder<Section> _ builder: () -> [Section]) {
self.sections = builder()
}

func section(_ section: SectionIdentifierType, @ViewModelBuilder<ItemIdentifierType> _ builder: () -> [ItemIdentifierType]) -> Section {
Section(section: section, content: builder())
}

func snapshot() -> NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType> {
var snapshot = NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType>()

// Add sections
snapshot.appendSections(sections.map { $0.section })

// Add items
sections.forEach { model in
snapshot.appendItems(model.content, toSection: model.section)
}
return snapshot
}

func debugString() -> String {
sections.reduce("", {
$0 + "\($1.section) section: \($1.content)"
})
}
}

And finally a generic result builder that can construct the data source

@resultBuilder
struct ViewModelBuilder<T> {

static func buildEither(first component: [T]) -> [T] {
return component
}

static func buildEither(second component: [T]) -> [T] {
return component
}

static func buildOptional(_ component: [T]?) -> [T] {
return component ?? []
}

static func buildExpression(_ expression: T) -> [T] {
return [expression]
}

static func buildExpression(_ expression: ()) -> [T] {
return []
}

static func buildBlock(_ components: [T]...) -> [T] {
return components.flatMap { $0 }
}

static func buildArray(_ components: [[T]]) -> [T] {
Array(components.joined())
}
}

That’s all folks.

Thanks for reading!

Hopefully you learn something from this and can use it to help efficiently generate view models and data sources in your application.

Please leave a comment below if you found this useful. You can download the entire repository from Github to see it in action.

--

--