#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, я использую только приведенный выше код.