1 year ago
#365709
thexande
UICollectionViewDiffableDataSource updates horizontally scrolling section incorrectly, but handles vertical section updates just fine
I’m attempting to create a collection view with 2 sections which supports live reloading of data. Leveraging UICollectionVIewDiffableDataSource
, this should be quite straightforward.
final class ViewController: UIViewController {
private let collectionView = UICollectionView(frame: .zero, collectionViewLayout: .init())
private var dataSource: UICollectionViewDiffableDataSource<Model.Section, Model.Item>?
var model = Model.default {
didSet {
applyModel()
}
}
typealias HorizontalRegistration = UICollectionView.CellRegistration<HorizontalView, Model.Item>
private let horizontalRegistration = HorizontalRegistration { cell, _, model in
guard case .horizontal(let props) = model else { return }
cell.model = props
}
typealias VerticalRegistration = UICollectionView.CellRegistration<VerticalView, Model.Item>
private let verticalRegistration = VerticalRegistration { cell, _, model in
guard case .vertical(let props) = model else { return }
cell.model = props
}
override func viewDidLoad() {
super.viewDidLoad()
configureDataSource()
collectionView.collectionViewLayout = makeLayout()
self.model = Model(sections: [
.init(items: (0..<20).map { .horizontal(.init(title: "\($0)", id: $0)) }, id: 1),
.init(items: (0..<20).map { .vertical(.init(title: "\($0)", id: $0)) }, id: 2),
])
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
var model = self.model
model.sections[0].items[0] = .horizontal(.init(title: "new", id: 0))
model.sections[1].items[0] = .vertical(.init(title: "new", id: 0))
self.model = model
}
}
override func loadView() {
view = collectionView
}
private func applyModel() {
var snap = NSDiffableDataSourceSnapshot<Model.Section, Model.Item>()
snap.appendSections(model.sections)
model.sections.forEach {
snap.appendItems($0.items, toSection: $0)
}
dataSource?.apply(snap)
}
private func configureDataSource() {
dataSource = .init(collectionView: collectionView) { [weak self] collectionView, indexPath, item in
guard let self = self else { return nil }
switch item {
case .horizontal:
return collectionView.dequeueConfiguredReusableCell(
using: self.horizontalRegistration,
for: indexPath,
item: item
)
case .vertical:
return collectionView.dequeueConfiguredReusableCell(
using: self.verticalRegistration,
for: indexPath,
item: item
)
}
}
}
}
extension ViewController {
struct Model: Hashable {
var sections: [Section]
static var `default`: Self { .init(sections: []) }
struct Section: Hashable {
var items: [Item]
let id: Int
static var `default`: Self { .init(items: [], id: -1) }
}
enum Item: Hashable {
case horizontal(HorizontalView.Model)
case vertical(VerticalView.Model)
}
}
}
My collection has 2 sections, one horizontally scrolling and one vertically scrolling. Using UICollectionViewCompositionalLayout
, constructing such a layout is quite simple.
private func makeLayout() -> UICollectionViewCompositionalLayout {
.init { [weak self] index, _ in
guard
let self = self,
self.model.sections.indices.contains(index),
let itemType = self.model.sections[index].items.first
else { return nil }
switch itemType {
case .vertical:
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(80)
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize,
subitem: item,
count: 1
)
let section = NSCollectionLayoutSection(group: group)
return section
case .horizontal:
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalWidth(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(
widthDimension: .absolute(160),
heightDimension: .absolute(218))
let group = NSCollectionLayoutGroup.vertical(
layoutSize: groupSize,
subitem: item,
count: 1)
group.contentInsets = NSDirectionalEdgeInsets(
top: 0,
leading: 0,
bottom: 0,
trailing: 0
)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 8
section.contentInsets = NSDirectionalEdgeInsets(
top: 24,
leading: 24,
bottom: 24,
trailing: 24
)
section.orthogonalScrollingBehavior = .groupPaging
return section
}
}
Each cell type has a simple view model, with an ID and a title. Synthesized Hashable
conformance should be enough for the diffable data source to determine I want to update an item, not remove it or move it.
final class HorizontalView: UICollectionViewCell {
var model = Model.default {
didSet {
label.text = model.title
}
}
private let label: UILabel = {
let label = UILabel()
label.font = .boldSystemFont(ofSize: 40)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
struct Model: Hashable {
let title: String
let id: Int
static var `default`: Self { .init(title: "", id: -1) }
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .green
addSubview(label)
label.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
final class VerticalView: UICollectionViewCell {
var model = Model.default {
didSet {
label.text = model.title
}
}
private let label: UILabel = {
let label = UILabel()
label.font = .boldSystemFont(ofSize: 40)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
struct Model: Hashable {
let title: String
let id: Int
static var `default`: Self { .init(title: "", id: -1) }
}
override init(frame: CGRect) {
super.init(frame: frame)
layer.borderWidth = 1
layer.borderColor = UIColor.black.cgColor
addSubview(label)
label.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
My issue is the following: The horizontal scrolling collection reloads the entire section any time I mutate the data source, even when the same identifiers are used as the previous view model. this results in the horizontal collection scrolling back to the beginning, as well as every item in the section flashing as it reloads.
Alternatively, the vertically scrolling collection functions correctly with the same mutation. Scrolling is not reset to the beginning, and the mutated item intelligently reloads with new model. It is this functionality I would like to replicate in the horizontally scrolling collection.
Am I utilizing UICollectionViewDiffableDatasource
incorrectly? Any ideas on why it works for the vertical section, but not the horizontal?
The project is available in a git repo here should anyone want to check it out on their machine. https://github.com/thexande/UICollectionViewDiffableDataSource_horizontal_reload_issue
ios
uicollectionview
uikit
uicollectionviewcell
uicollectionviewdiffabledatasource
0 Answers
Your Answer