13

I'm looking at DiffableDataSource available in iOS13 (or backported here: https://github.com/ra1028/DiffableDataSources) and cannot figure out how one would support multiple cell types in your collection or tableview.

Apple's sample code1 has:

var dataSource: UICollectionViewDiffableDataSource<Section, OutlineItem>! = nil

which seems to force a data source to be a single cell type. If I create a separate data source for another cell type - then there is no guarantee that both data sources don't have apply called on them at the same time - which would lead to the dreaded NSInternalInconsistencyException - which is familiar to anyone who has attempted to animate cell insertion/deletion manually with performBatchUpdates.

Am I missing something obvious?

Jason Moore
  • 7,169
  • 1
  • 44
  • 45

3 Answers3

23

I wrapped my different data in an enum with associated values. In my case, my data source was of type UICollectionViewDiffableDataSource<Section, Item>, where Item was

enum Item: Hashable {
  case firstSection(DataModel1)
  case secondSection(DataModel2)
}

then in your closure passed into the data source's initialization, you get an Item, and you can test and unwrap the data, as needed.

(I'd add that you should ensure that your backing associated values are Hashable, or else you'll need to implement that. That is what the diff'ing algorithm uses to identify each cell, and resolve movements, etc)

oflannabhra
  • 661
  • 5
  • 18
9

You definitely need to have a single data source.

The key is to use a more generic type. Swift's AnyHashable works well here. And you just need to cast the instance of AnyHashable to a more specific class.

lazy var dataSource = CollectionViewDiffableDataSource<Section, AnyHashable> (collectionView: collectionView) { collectionView, indexPath, item in

        if let article = item as? Article, let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Section.articles.cellIdentifier, for: indexPath) as? ArticleCell {
            cell.article = article
            return cell
        }

        if let image = item as? ArticleImage, let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Section.trends.cellIdentifier, for: indexPath) as? ImageCell {
            cell.image = image
            return cell
        }

        fatalError()
    }

And the Section enum looks like this:

    enum Section: Int, CaseIterable {
        case articles
        case articleImages

        var cellIdentifier: String {
            switch self {
            case .articles:
                return "articleCell"
            case .articleImages:
                return "imagesCell"
            }
        }
    }

Jason Moore
  • 7,169
  • 1
  • 44
  • 45
0

One way of achieving this could be taking advantage your Section enum to identify the section with indexPath.section. It will be something like this:

lazy var dataSource = UICollectionViewDiffableDataSource<Section, Item> (collectionView: collectionView) { collectionView, indexPath, item in

        let section = Section(rawValue: indexPath.section)
        switch section {
        case .firstSection:
             let cell = ... Your dequeue code here for first section ...
             return cell
        case .secondSection:
             let cell = ... Your dequeue code here for second section ...
             return cell
        default:
             fatalError() // Here is handling the unmapped case that should not happen
        }
    }