1

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:

  • First section: with some rows/items below each other
  • Second section: like the first section, but the rows/items are selectable
  • Third section: the buttons section. This section is ALWAYS present. It has at least one button in it. This section always contains 1 cell: the cell contains a horizontal stack view with buttons. But, as I said, there is always 1 button, at least. By checking/unchecking the rows/items from section 2, there is an extra button in the buttons section. When no row is selected --> no extra button. When checking extra rows, the title of the button changes, according to the number of selected/checked rows: --> "Send (5)" for example. When only 4 rows are checked, this title needs to change to "Send (4)". When no rows are selected, this button should be hidden.

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.

Charlotte1993
  • 589
  • 4
  • 27
  • "I removed some stuff that I think is not relevant here" But since the issue lies in what your data model consists of (e.g. the details of what your data source _is_), you removed exactly the wrong stuff. The layout is not all that important; the diffable data source is what matters. In any case, jumping diffable data sources have been thoroughly dealt with here, so a search might help you. – matt Dec 13 '21 at 13:39
  • @matt I just figured out that if I change the `.estimated` value of height of an item of my vertical group in my compositional layout, the offset of the scroll is different. On a short list of items in the second section, I see with a different `estimated height` value still the border of my buttons cell. On a very large list of second section items, the offset after scrolling is much larger, so I think it has something to do with that. The diffable data source implementation seems fine. – Charlotte1993 Dec 13 '21 at 13:46

1 Answers1

0

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])

Charlotte1993
  • 589
  • 4
  • 27