Properly reload a UICollectionViewDiffableDataSource

448 views Asked by At

So I'm having a screen with a Diffable DataSource List CollectionView that will be reloaded when a ManagedContext will be changed.

My problem with the reload right now is that cells keep on randomly changing and most of the times they duplicate themselves with items evem from different sections.

What would be a good solution to reload this?

class MultiSectionExpandableListView: UIView {
weak var delegate: MultiSectionExpandableListViewDelegate?
private var objects: [HeaderItem]
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<HeaderItem, ItemType>!
private lazy var noEntriesView: NoEntriesView = {
    let view = NoEntriesView.instantiateNib()
    view.frame = collectionView.bounds
    return view
}()

init(frame: CGRect, objects: [HeaderItem]) {
    self.objects = objects
    super.init(frame: frame)

    setupCollectionView()
}
    
required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

private func setupCollectionView() {
    var layoutConfig = UICollectionLayoutListConfiguration(appearance: .sidebar)
    layoutConfig.headerMode = .firstItemInSection
    layoutConfig.showsSeparators = false
    layoutConfig.backgroundColor = .clear
    let listLayout = UICollectionViewCompositionalLayout.list(using: layoutConfig)
        
    collectionView = UICollectionView(frame: bounds, collectionViewLayout: listLayout)
    collectionView.delegate = self
    collectionView.backgroundColor = .clear
    addSubview(collectionView)

    collectionView.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
        collectionView.topAnchor.constraint(equalTo: topAnchor),
        collectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
        collectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
        collectionView.bottomAnchor.constraint(equalTo: bottomAnchor)])

    guard objects[0].items.count > 0 || objects[1].items.count > 0 || objects[2].items.count > 0 || objects[3].items.count > 0 else {
        collectionView.backgroundView = noEntriesView
        return
    }
    
    let headerCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, HeaderItem> {
        (cell, indexPath, headerItem) in
        var content = cell.defaultContentConfiguration()
        content.text = headerItem.title
        cell.contentConfiguration = content
        
        var headerDisclosureOption = UICellAccessory.OutlineDisclosureOptions(style: .header)
        headerDisclosureOption.tintColor = AppStorage.tintColor.uiColor()
        cell.accessories = [.outlineDisclosure(options:headerDisclosureOption)]
    }

    let itemCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, ListItem> {
        (cell, indexPath, listItem) in
        
        var configuration = cell.defaultContentConfiguration()
        configuration.text = listItem.title
        configuration.secondaryText = listItem.description
        configuration.imageProperties.maximumSize = CGSize(width: 50, height: 50)
        configuration.imageProperties.reservedLayoutSize = CGSize(width: 60, height: 60)
        let spinner = UIActivityIndicatorView(style: .large)
        spinner.color = AppStorage.tintColor.uiColor()
        spinner.startAnimating()
        cell.accessories = [.customView(configuration: UICellAccessory.CustomViewConfiguration(customView: spinner, placement: .leading(displayed: .always)))]
        cell.contentConfiguration = configuration
        
        DownloadImageService.shared.loadImage(stringUrl: listItem.imageURL) { (data, error) in
            guard error == nil, let data = data else { return }
            configuration.image = UIImage(data: data)
            cell.accessories = []
            cell.contentConfiguration = configuration
        }
    }

    dataSource = UICollectionViewDiffableDataSource<HeaderItem, ItemType>(collectionView: collectionView) {
        (collectionView, indexPath, itemType) -> UICollectionViewCell? in
        switch itemType {
        case .header(let headerItem):
            let cell = collectionView.dequeueConfiguredReusableCell(using: headerCellRegistration, for: indexPath, item: headerItem)
            return cell
        case .cell(let listItem):
            let cell = collectionView.dequeueConfiguredReusableCell(using: itemCellRegistration, for: indexPath, item: listItem)
            cell.backgroundConfiguration?.backgroundColor = .clear
            return cell
        }
    }

    var currentSnapshot = NSDiffableDataSourceSnapshot<HeaderItem, ItemType>()
    currentSnapshot.appendSections(objects)
    dataSource.apply(currentSnapshot)

    for headerItem in objects {
        var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<ItemType>()
        let headerListItem = ItemType.header(headerItem)
        sectionSnapshot.append([headerListItem])

        let listItemArray = headerItem.items.map { ItemType.cell($0) }
        sectionSnapshot.append(listItemArray, to: headerListItem)
        sectionSnapshot.expand([headerListItem])

        dataSource.apply(sectionSnapshot, to: headerItem, animatingDifferences: false)
    }
}

func reloadCollectionView(with newObjects: [HeaderItem]) {
    objects = newObjects
    guard objects[0].items.count > 0 || objects[1].items.count > 0 || objects[2].items.count > 0 || objects[3].items.count > 0 else {
        collectionView.backgroundView = noEntriesView
        return
    }
    var currentSnapshot = dataSource.snapshot()
    currentSnapshot = NSDiffableDataSourceSnapshot<HeaderItem, ItemType>()
    currentSnapshot.appendSections(objects)
    dataSource.apply(currentSnapshot)
    for headerItem in objects {
        var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<ItemType>()
        let headerListItem = ItemType.header(headerItem)
        sectionSnapshot.append([headerListItem])

        let listItemArray = headerItem.items.map { ItemType.cell($0) }
        sectionSnapshot.append(listItemArray, to: headerListItem)
        sectionSnapshot.expand([headerListItem])

        dataSource.apply(sectionSnapshot, to: headerItem, animatingDifferences: false)
    }
}

}

And also here are my objects:

struct ListItem: Hashable {
    let title: String
    let description: String
    let imageURL: String
}

struct HeaderItem: Hashable {
    let title: String
    let items: [ListItem]
}

enum ItemType: Hashable {
    case header(HeaderItem)
    case cell(ListItem)
}
0

There are 0 answers