Charlotte1993
Charlotte1993

Reputation: 609

Compositional UICollectionView with NSDiffableDataSource jumps when reloading

I have a compositional layout for my UICollectionView. This is the code for creating the layout.

func createLayout() -> UICollectionViewLayout {
    let layout = UICollectionViewCompositionalLayout { [weak self] section, _ -> NSCollectionLayoutSection? in
        guard 
            let self = self,
            let sections = self.viewModel?.sections,
            let sectionData = sections[safe: section] else { return nil }

            switch sectionData {
            case .firstSection:
                return self.createFirstSectionSection()
            case .secondSection:
                return self.createSecondSection()
            case .buttons(_, let topSpacing):
                return self.createButtonsSection(topSpacing: topSpacing)
            }
        }

        let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                heightDimension: .estimated(108))

        let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize,
                                                                 elementKind: "header",
                                                                 alignment: .top)

        let config = UICollectionViewCompositionalLayoutConfiguration()
        config.boundarySupplementaryItems = [header]
        config.scrollDirection = .vertical
        config.interSectionSpacing = 0

        layout.configuration = config

        return layout
    }

    func createFirstSection() -> NSCollectionLayoutSection {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(144))

        let item = NSCollectionLayoutItem(layoutSize: itemSize, supplementaryItems: [borderItem])
        let group = NSCollectionLayoutGroup.vertical(layoutSize: itemSize, subitems: [item])

        group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 60, bottom: 0, trailing: 20)

        let layoutSection = NSCollectionLayoutSection(group: group)

        return layoutSection
    }

    func createSecondSection() -> NSCollectionLayoutSection {
        let borderItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
        let borderItem  = NSCollectionLayoutSupplementaryItem(layoutSize: borderItemSize, elementKind: "item-border-view", containerAnchor: NSCollectionLayoutAnchor(edges: .top))

        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(58))

        let item = NSCollectionLayoutItem(layoutSize: itemSize, supplementaryItems: [borderItem])
        let group = NSCollectionLayoutGroup.vertical(layoutSize: itemSize, subitems: [item])

        group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: hasCheckboxes ? 20 : 60, bottom: 0, trailing: 20)

        let layoutSection = NSCollectionLayoutSection(group: group)

        return layoutSection
    }

    func createButtonsSection(topSpacing: CGFloat) -> NSCollectionLayoutSection {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(41))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)

        let group = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: itemSize.widthDimension, heightDimension: itemSize.heightDimension), subitems: [item])

        let section = NSCollectionLayoutSection(group: group)
        section.contentInsets = NSDirectionalEdgeInsets(top: topSpacing, leading: 60, bottom: 0, trailing: 20)

        return section
    }

My model looks like this:

enum Section {
    case firstSection(items: [FirstSectionItem])
    case secondSection(items: [SecondSectionItem])
    case buttons(cellViewModel: ButtonsCellViewModel, topSpacing: CGFloat)

    var items: [AnyHashable] {
        switch self {
        case .firstSection(let firstSectionItems):
            return firstSectionItems
        case .quotes(let secondSectionItems):
            return secondSectionItems
        case .buttons(let cellViewModel, _):
            return [cellViewModel]
        }
    }
}

// MARK: - Hashable

extension Section: Hashable {

    static func == (lhs: Section, rhs: Section) -> Bool {
        switch (lhs, rhs) {
        case (.firstSection(let leftItems), .firstSection(let rightItems)):
            return leftItems == rightItems
        case (.secondSection(let leftItems), .secondSection(let rightItems)):
            return leftItems == rightItems
        case (.buttons(let leftCellViewModel, let leftTopSpacing), .buttons(let rightCellViewModel, let rightTopSpacing)):
            return true
        default:
            return false
        }
    }

    func hash(into hasher: inout Hasher) {
        switch self {
        case .firstSection(let items):
            hasher.combine(items)
        case .secondSection(let items):
            hasher.combine(items)
        case .buttons(let cellViewModel, let topSpacing):
            hasher.combine("Same") // I use this to make sure that there is no difference in the buttons section. What I try to accomplish is that the buttons section (section at the bottom) does not animate out of screen to reload it's UI.
        }
    }
}

The data model is much more complex, but for the sake of the question, I removed some stuff that I think is not relevant here and will only create clutter.

The reloading of the collectionView with DiffableDataSource looks like this:

func refreshUI() {
    guard let viewModel = viewModel else { return }

    let newDataSource = WorkApprovalDataSource(sections: viewModel.sections)

    var snapshot = NSDiffableDataSourceSnapshot<APIWorkApprovalSection, AnyHashable>()

    newDataSource.sections.forEach {
        snapshot.appendSections([$0])
        snapshot.appendItems($0.items, toSection: $0)
    }

    dataSource?.apply(snapshot, animatingDifferences: true)
}

The point is, I want 3 sections on screen:

I've had trouble since the beginning with the reloading of the sections. It jumps up and down. When checking the rows from section 2, and de buttons section is not visible, because the item list of section 2 is too large for example, the first time checking/selecting a row, it jumps. After that, if the buttons section is still not on the screen, selecting and deselecting rows is no problem, no jump occurs.

But: when I scroll to the bottom, so that the buttons section is visible, and then I select a row, the collection View scrolls a bit so that the buttons are out of sight. When I scroll the buttons in sight again, the data in the cells looks fine, so the reload happens "correctly". What I want is that the buttons section doesn't scroll out of screen to reload the UI. I've handled this by making the Hashable protocol always hash the same text, so there is no difference, right? The change of the title of the button and the visibility of it, I handle via the cellViewModel of the buttons. So that works perfectly fine. But the buttons keep scrolling out of sight to reload. And I don't know what is causing it.

I really need the compositional layout for decoration items and stuff, so I cannot drop this.

Thanks in advance for taking a look and maybe posting some suggestions/fixes.

Upvotes: 1

Views: 1836

Answers (1)

Charlotte1993
Charlotte1993

Reputation: 609

I finally figured it out. You have to set the dimension of the layout group of the vertical scrolling items, to horizontal instead of vertical. I have literally read dozens of tutorials where this was not mentioned, not even in the Apple documentation. I've wasted days, if not weeks on this stupid scrolling thing. So conclusion: Diffable DataSources works just fine, it was the compositional layout that was configured in the wrong way.

So instead of doing this for a vertical scrolling list:

let group = vertical(layoutSize: itemSize, subitems: [item])

You have to do this:

let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitems: [item])

Upvotes: 1

Related Questions