UITableViewDiffableDataSource with different objects
Asked Answered
C

3

13

I am currently facing troubles using UITableViewDiffableDataSource.

I would like to give a shot to this new feature, so I went on many tutorials on the net, but none of them seems to answer my issue.

In my current viewController I have a UITableView, with 3 different objects (with different types each), but the UITableViewDiffableDataSource is strongly typed to one.

Like: dataSource = UITableViewDiffableDataSource <SectionType, ItemType>

All my sections are fed with something like

func numberOfSections(in tableView: UITableView) -> Int {
    return 3
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if section == 0 {        
        return bigObject.ObjectsOfType1.count
    } else if section == 1 {
        return bigObject.ObjectsOfType2.count
    } else {
        return bigObject.ObjectsOfType3.count
    }
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) as! CustomTableViewCell
    if indexPath.section == 0 {
        cell.buildWithFirstObject(obj: bigObject.ObjectsOfType1[indexPath.row])
    } else if indexPath.section == 1 {
        cell.buildWithFirstObject(obj: bigObject.ObjectsOfType2[indexPath.row])
    } else {
        cell.buildWithFirstObject(obj: bigObject.ObjecstOfType3[indexPath.row])
    }
}

Is there a trick to use diffable dataSource in my case ?

Any help is appreciated ! Thank you for reading me :)

Chadd answered 27/11, 2019 at 14:6 Comment(2)
What kind of trouble you are facing into your this code?Lactiferous
Using the diffable data source seems to allow me to only load data for one section, because of the "Item Type" being different for every section.Chadd
R
7

For the simplest approach, I would suggest using AnyHashable for the item identifier and enum for the section identifier. I would avoid using an enumeration for the item identifier because it can quickly get out of hand constantly employing switches just to unwrap generic enumerations into custom types, which is not only onerous but doesn't read well at all.

// MARK: SECTION IDENTIFIERS

private enum Section {
    case title
    case kiwis
    case mangos
}

// MARK: DATA MODELS

private struct Title: Hashable {}

private struct Kiwi: Hashable {
    let identifier = UUID().uuidString
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(identifier)
    }
    
    static func == (lhs: Kiwi, rhs: Kiwi) -> Bool {
        return lhs.identifier == rhs.identifier
    }
}

private struct Mango: Hashable {
    let identifier = UUID().uuidString
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(identifier)
    }
    
    static func == (lhs: Mango, rhs: Mango) -> Bool {
        return lhs.identifier == rhs.identifier
    }
}

// MARK: DATA SOURCE

private var dataSource: UITableViewDiffableDataSource<Section, AnyHashable>!

dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
    switch item {
    case is Title:
        return TitleCell()
        
    case let item as Kiwi:
        let cell = tableView.dequeueReusableCell(withIdentifier: KiwiCell.identifer, for: indexPath) as? KiwiCell
        cell?.label.text = item.identifier
        return cell
        
    case let item as Mango:
        let cell = tableView.dequeueReusableCell(withIdentifier: MangoCell.identifier, for: indexPath) as? MangoCell
        cell?.label.text = item.identifier
        return cell
    default:
        return nil
    }
})

// MARK: USAGE

var initialSnapshot = NSDiffableDataSourceSnapshot<Section, AnyHashable>()
initialSnapshot.appendSections([.title, .kiwis, .mangos])
initialSnapshot.appendItems([Title()], toSection: .title)
initialSnapshot.appendItems([k1, k2, k3], toSection: .kiwis)
initialSnapshot.appendItems([m1, m2, m3], toSection: .mangos)
self.dataSource.apply(initialSnapshot, animatingDifferences: false)
Rodman answered 7/12, 2021 at 2:34 Comment(0)
E
13

Another possibility, that would prevent casting NSObject to whatever you are expecting (which could be crash-prone), is to wrap your different objects as associated values in an enum that conforms to Hashable. Then, in your dequeue callback, you get the enum, and can unwrap the associated value. So something like

enum Wrapper: Hashable {
  case one(Type1)
  case two(Type2)
  case three(Type3)
}

dataSource = UITableViewDiffableDataSource <SectionType, Wrapper>(collectionView: collectionView!) { [weak self] (collectionView: UICollectionView, indexPath: IndexPath, wrapper: Wrapper) -> UICollectionViewCell? in
  switch wrapper {
    case .one(let object):
      guard let cell = dequeueReusableCell( ... ) as? YourCellType else { fatalError() }
      // configure the cell
      cell.prop1 = object.prop1
      return cell

    case .two(let object2):
      guard let cell = dequeueReusableCell( ... ) as? YourCellType2 else { fatalError() }
      // configure the cell
      cell.prop1 = object2.prop1
      return cell

    case .three(let object3):
      guard let cell = dequeueReusableCell( ... ) as? YourCellType3 else { fatalError() }
      // configure the cell
      cell.prop1 = object3.prop1
      return cell
  }
}

You could probably even simplify that with a single return.

Enisle answered 17/1, 2020 at 15:19 Comment(5)
That's a nice one ! I'll try it ! For now the thing I've done is make my different objects inherits from NSObject.Chadd
I've also come across the same problem as OP, and this was super helpful! However I am having difficulty setting up my snapshot properly. How would you go about setting up the NSDiffableDataSourceSnapshot for the example you porivlded? I can't figure out how to get it working with 2 different data types (one of which is an array and the other is not).Agrapha
@Agrapha that depends on how you are structuring your data, and how you are presenting it. You could do something like Swift let newSnapshot = NSDiffableDataSourceSnapshot<Section, Wrapper>() newSnapshot.appendSections([Section.first, Section.last]) newSnapshot.appendItems(allItems.filter { $0 == .one}, toSection: .first) newSnapshot.appendItems(allItems.filter { $0 == .three}, toSection: .last) Enisle
@Enisle Thanks for posting this solution, but I am struggling when the associated value is an array like case three([Type3]) . How should I append it in the snapshot to apply?. If I do snapShot.appendItems(wrapper3, toSection: section) it will append only a single element to that sectionSomebody
If you are using an array, you will need to handle merging the arrays prior to your call to appendItems(). If you pass an Array to that function, it will add the whole array as a separate item. I’d recommend rethinking how you are storing your data.Enisle
R
7

For the simplest approach, I would suggest using AnyHashable for the item identifier and enum for the section identifier. I would avoid using an enumeration for the item identifier because it can quickly get out of hand constantly employing switches just to unwrap generic enumerations into custom types, which is not only onerous but doesn't read well at all.

// MARK: SECTION IDENTIFIERS

private enum Section {
    case title
    case kiwis
    case mangos
}

// MARK: DATA MODELS

private struct Title: Hashable {}

private struct Kiwi: Hashable {
    let identifier = UUID().uuidString
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(identifier)
    }
    
    static func == (lhs: Kiwi, rhs: Kiwi) -> Bool {
        return lhs.identifier == rhs.identifier
    }
}

private struct Mango: Hashable {
    let identifier = UUID().uuidString
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(identifier)
    }
    
    static func == (lhs: Mango, rhs: Mango) -> Bool {
        return lhs.identifier == rhs.identifier
    }
}

// MARK: DATA SOURCE

private var dataSource: UITableViewDiffableDataSource<Section, AnyHashable>!

dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
    switch item {
    case is Title:
        return TitleCell()
        
    case let item as Kiwi:
        let cell = tableView.dequeueReusableCell(withIdentifier: KiwiCell.identifer, for: indexPath) as? KiwiCell
        cell?.label.text = item.identifier
        return cell
        
    case let item as Mango:
        let cell = tableView.dequeueReusableCell(withIdentifier: MangoCell.identifier, for: indexPath) as? MangoCell
        cell?.label.text = item.identifier
        return cell
    default:
        return nil
    }
})

// MARK: USAGE

var initialSnapshot = NSDiffableDataSourceSnapshot<Section, AnyHashable>()
initialSnapshot.appendSections([.title, .kiwis, .mangos])
initialSnapshot.appendItems([Title()], toSection: .title)
initialSnapshot.appendItems([k1, k2, k3], toSection: .kiwis)
initialSnapshot.appendItems([m1, m2, m3], toSection: .mangos)
self.dataSource.apply(initialSnapshot, animatingDifferences: false)
Rodman answered 7/12, 2021 at 2:34 Comment(0)
C
1

It seems that using UITableViewDiffableDataSource<Section, NSObject> and having my different object inherits from NSObject works fine.

Chadd answered 27/11, 2019 at 14:51 Comment(2)
Instead of using NSObject, you can use Generics.Karisa
Or just HashableLundin

© 2022 - 2024 — McMap. All rights reserved.