UICollectionView — снимки разделов — «Не удалось найти индекс элемента» при удалении разделов или элементов

#ios #swift #uicollectionview #nsdiffabledatasourcesnapshot

#iOS #swift #uicollectionview #nsdiffabledatasourcesnapshot

Вопрос:

Я хочу создать collectionView динамические разделы, которые также можно свернуть. Кажется, это довольно просто с новым section snapshots в iOS 14. Это то, что у меня есть (полностью рабочий пример).

 import UIKit

enum Section: Hashable {
    case group(Int)
}

enum Item: Hashable {
    case header(Int)
    case item(String)
}

class ViewController: UIViewController {

    private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!

    private var groups = [1, 2, 3, 4, 5]
    private var groupItems: [Int: [String]] = [
        1: ["A", "B", "C", "D"],
        2: ["E", "F", "G", "H"],
        3: ["I", "J", "K", "L"],
        4: ["M", "N", "O", "P"],
        5: ["Q", "R", "S", "T"],
    ]
    
    private lazy var collectionView: UICollectionView = {
        var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        config.headerMode = .firstItemInSection
        
        let layout = UICollectionViewCompositionalLayout.list(using: config)
        let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
        
        collectionView.backgroundColor = .systemGroupedBackground
        
        return collectionView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(collectionView)
        collectionView.frame = view.bounds
        
        configureDataSource()
        applySnapshot()
        
        DispatchQueue.main.asyncAfter(deadline: .now()   .seconds(2)) {
            self.groups.remove(at: 0) // This removes the entire first section
            self.applySnapshot()
        }
    }
    
    private func configureDataSource() {
        
        let itemCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> {
            (cell, indexPath, letter) in
            
            var content = cell.defaultContentConfiguration()
            content.text = letter
            cell.contentConfiguration = content

        }
        
        let headerCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> {
            (cell, indexPath, number) in
            
            var content = cell.defaultContentConfiguration()
            
            content.text = String(number)
            cell.contentConfiguration = content
            
            let headerDisclosureOption = UICellAccessory.OutlineDisclosureOptions(style: .header)
            cell.accessories = [.outlineDisclosure(options: headerDisclosureOption)]
        }
        
        dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: { (collectionView, indexPath, item) -> UICollectionViewCell? in
            
            switch item {
            case .header(let number):
                return collectionView.dequeueConfiguredReusableCell(using: headerCellRegistration, for: indexPath, item: number)
            case .item(let letter):
                return collectionView.dequeueConfiguredReusableCell(using: itemCellRegistration, for: indexPath, item: letter)
            }
        })
    }

    private func applySnapshot() {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()

        snapshot.appendSections(groups.map { .group($0) })
        
        // This line causes the error messages. If I comment it out, the messages go away but changing sections (removing or adding some) doesn't work any more (the collectionview does not reflect the changes of the sections)
        dataSource.apply(snapshot, animatingDifferences: false)
        
        for group in groups {
            var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
            
            // header
            let headerItem = Item.header(group)
            sectionSnapshot.append([headerItem])
            
            // items
            sectionSnapshot.append((groupItems[group] ?? []).map { Item.item($0) }, to: headerItem)
            
            sectionSnapshot.expand([headerItem])
            
            dataSource.apply(sectionSnapshot, to: .group(group))
        }
    }

}
  

Это всего лишь простое представление коллекции, показывающее несколько разделов по 4 элемента в каждом. Чтобы продемонстрировать мою проблему, я добавил закрытие, которое автоматически вызывается через 2 секунды после загрузки контроллера представления. Он удаляет первый раздел и обновляет источник данных collectionview.

Есть две проблемы:

Во-первых, он выдает мне следующие сообщения об ошибках:

 sectionSnapshotExample[15380:1252802] [DiffableDataSource] Failed to find index of item sectionSnapshotExample.Item.header(1)
sectionSnapshotExample[15380:1252802] [DiffableDataSource] Failed to find index of item sectionSnapshotExample.Item.header(4)
sectionSnapshotExample[15380:1252802] [DiffableDataSource] Failed to find index of item sectionSnapshotExample.Item.header(2)
sectionSnapshotExample[15380:1252802] [DiffableDataSource] Failed to find index of item sectionSnapshotExample.Item.header(3)
sectionSnapshotExample[15380:1252802] [DiffableDataSource] Failed to find index of item sectionSnapshotExample.Item.header(5)
  

Во-вторых, обновление визуально неприятно, потому что оно перезагружает весь вид коллекции (все ячейки переходят друг в друга, даже если они не изменились).

Ошибка возникает внутри applySnapshot() метода:

 private func applySnapshot() {
    var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()

    snapshot.appendSections(groups.map { .group($0) })
    

    // This line causes the error messages
    dataSource.apply(snapshot, animatingDifferences: false)
    
    ....
}
  

Это применяет разделы к источнику данных (Apple делает то же самое в своих примерах проектов).

Если я прокомментирую эту строку, разделы больше не обновляются (в этом примере удаленный раздел останется в представлении коллекции.

Есть идеи? Я делаю это неправильно? section snapshots Не предназначены для использования с динамическим контентом? Если да, есть ли другой простой способ иметь сворачиваемые разделы?

Спасибо!

Комментарии:

1. Я думаю, что ошибка «[DiffableDataSource] Не удалось найти индекс элемента» вызвана строкой «sectionSnapshot.expand([headerItem])». У меня аналогичная проблема, и без расширения раздела я не получаю эту ошибку. Проблема с анимацией все еще присутствует — все ячейки перезагружаются.

2. Вы когда-нибудь находили решение для этого?

3. @Anthony Я нашел решение

Ответ №1:

Я столкнулся с этой проблемой ранее сегодня, и этот пост был единственной ссылкой на него, которую я мог найти где угодно в Интернете.

После нескольких часов работы я нашел исправление. Похоже, что UICollectionViewDiffableDataSource возникают проблемы с автоматическим различением при попытке развернуть заголовки для уже существующих разделов при применении моментального снимка с новым содержимым.

Поэтому вместо того, чтобы всегда полностью автоматически вычислять разницу, создавая новый снимок верхнего уровня без содержимого, вам нужно, чтобы ваш источник данных генерировал снимок, проверял, существуют ли разделы, в которые вы добавляете содержимое, в моментальном снимке, а затем применял новый снимок верхнего уровня, только если естьотсутствуют разделы.

На практике это выглядит как переход от этого…

 func applySnapshot(with cheeses: [Cheese]?) {
    var snapshot = NSDiffableDataSourceSnapshot<CheeseType, ListItem>()

    snapshot.appendSections(CheeseType.allCases)
    dataSource?.apply(snapshot)
    
    for cheeseType in CheeseType.allCases {
        var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<ListItem>()
        
        let header = ListItem.header(cheeseType)
        sectionSnapshot.append([header])
        sectionSnapshot.expand([header])
        
        if let cheeses = cheeses {
            let matchingCheese = cheeses.filter { $0.cheeseType == cheeseType }
            let cheeseItems = matchingCheese.map { ListItem.content($0) }
            sectionSnapshot.append(cheeseItems, to: header)
        }
        
        dataSource?.apply(sectionSnapshot, to: cheeseType)
    }
}
  

… к чему-то вроде этого.

 func applySnapshot(with cheeses: [Cheese]?) {
    /* Instead of creating a blank snapshot every time,
       source a snapshot from dataSource if possible and
       only create a blank snapshot if that's not possible */
    var snapshot = dataSource?.snapshot() ?? NSDiffableDataSourceSnapshot<CheeseType, ListItem>()

    /* Need to prevent sections that already exist in
       the snapshot from being added again.
       In this case, checking if sectionIdentifiers is
       empty is adequate since sections never change -
       other cases may need more involved checking */
    if snapshot.sectionIdentifiers.isEmpty {
        snapshot.appendSections(CheeseType.allCases)
        dataSource?.apply(snapshot)
    }
    
    for cheeseType in CheeseType.allCases {
        var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<ListItem>()
        
        let header = ListItem.header(cheeseType)
        sectionSnapshot.append([header])
        sectionSnapshot.expand([header])
        
        if let cheeses = cheeses {
            let matchingCheese = cheeses.filter { $0.cheeseType == cheeseType }
            let cheeseItems = matchingCheese.map { ListItem.content($0) }
            sectionSnapshot.append(cheeseItems, to: header)
        }
        
        dataSource?.apply(sectionSnapshot, to: cheeseType)
    }
}
  

Ответ №2:

У меня была та же ошибка, и мое решение было:

     var collapseSectionSnapshot = NSDiffableDataSourceSectionSnapshot<CollapsibleElementType>()
    elements.forEach { collapseHeader in
        let item = CollapsibleElementType.header(collapseHeader)
        collapseSectionSnapshot.append([item])
        let collpasibleElement = collapseHeader.collapsibleElements.map({CollapsibleElementType.element($0)})
        collapseSectionSnapshot.append(collpasibleElement, to: item)
    }
    dataSource.apply(collapseSectionSnapshot, to: .main, animatingDifferences: false)
  

Я не создаю NSDiffableDataSourceSnapshot, я использую только приведенный выше код.