diffable 数据源部分 header 在更新期间闪烁
diffable data source section header blinks during update
我目前面临的问题是,在将新快照应用到我当前的数据源时,header、页脚和装饰视图不是 collection 视图子视图的一部分,可以注意到这是一种奇怪的闪烁。有人以前遇到过这个问题吗?
我通过以下方式更新数据源:
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections(Sections.allCases)
items.forEach { snapshot.appendItems([[=11=]], toSection: ItemSectionMapper.getSection(for: [=11=])) }
self.dataSource?.apply(snapshot)
编辑:
它似乎只发生在 iOS 14 台设备上。
编辑2:
这是示例项目中同一问题的屏幕录像:
https://imgur.com/a/0rS9aZU
下面的代码:
import UIKit
// MARK: - Cell -
final class Cell: UICollectionViewCell {
static let reuseIdentifier = "Cell"
var isExpanded = false {
didSet { label.numberOfLines = numberOfLines }
}
var numberOfLines: Int { isExpanded ? 0 : 3 }
lazy var label: UILabel = {
let label = UILabel()
label.numberOfLines = numberOfLines
label.frame.size = contentView.bounds.size
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(label)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
label.sizeThatFits(size)
}
}
final class Header: UICollectionReusableView {
static let elementKind = "Header"
lazy var label: UILabel = {
let label = UILabel()
label.numberOfLines = 1
label.frame.size = bounds.size
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(label)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
label.sizeThatFits(size)
}
}
// MARK: - UIViewController -
class ViewController: UIViewController {
struct Item: Hashable {
let text: String
var isExpanded = false
private let uuid = UUID()
}
var items: [Item] = [
.init(
text: """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore
eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident.
"""
),
.init(
text: """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore
eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident.
""",
isExpanded: true
)
]
lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createCollectionViewLayout())
collectionView.register(Cell.self, forCellWithReuseIdentifier: Cell.reuseIdentifier)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.contentInset.top = 44
collectionView.backgroundColor = .white
collectionView.delegate = self
return collectionView
}()
lazy var dataSource = UICollectionViewDiffableDataSource<Int, Item>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.reuseIdentifier, for: indexPath) as? Cell else { fatalError() }
cell.isExpanded = itemIdentifier.isExpanded
cell.label.text = itemIdentifier.text
return cell
}
override func viewDidLoad() {
super.viewDidLoad()
dataSource.supplementaryViewProvider = { (collectionView, kind, indexPath) -> UICollectionReusableView? in
guard let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: Header.elementKind, for: indexPath) as? Header else { fatalError() }
view.label.text = "Test"
return view
}
view.addSubview(collectionView)
collectionView.register(Header.self, forSupplementaryViewOfKind: Header.elementKind, withReuseIdentifier: Header.elementKind)
updateSnapshot()
}
private func createCollectionViewLayout() -> UICollectionViewCompositionalLayout {
let layoutSize = NSCollectionLayoutSize.init(
widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(200)
)
let section = NSCollectionLayoutSection(group:
.vertical(
layoutSize: layoutSize,
subitems: [.init(layoutSize: layoutSize)]
)
)
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .estimated(20)), elementKind: Header.elementKind, alignment: .top)
section.boundarySupplementaryItems = [header]
section.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
section.interGroupSpacing = 20
return .init(section: section)
}
private func updateSnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<Int, Item>()
snapshot.appendSections([0])
snapshot.appendItems(items)
dataSource.apply(snapshot, animatingDifferences: true)
}
}
// MARK: - UICollectionViewDelegate -
extension ViewController: UICollectionViewDelegate {
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let itemIdentifier = dataSource.itemIdentifier(for: indexPath) else { return }
items[indexPath.row] = .init(text: itemIdentifier.text, isExpanded: !itemIdentifier.isExpanded)
updateSnapshot()
}
}
感谢@JWK
这种行为似乎出乎意料,但我相信它的发生仅仅是因为整个部分在 apply(_:animatingDifferences:completion:)
中更新了动画。您可以尝试一些解决方法:
- 调用
apply(_:animatingDifferences:completion:)
时将animatingDifferences
设置为false
。如果你想要动画就不理想了。
- 添加另一个部分而不是使用
boundarySupplementaryItems
。不展开的部分不应在视觉上受到影响。您可能需要引入另一个单元格并为此使用 UICollectionViewCompositionalLayout
的 init(sectionProvider:)
(为每个部分提供正确的 NSCollectionLayoutSection
)。
- 如果您使用的是 iOS 14+,我认为您可以通过将
UICollectionViewListCell
的 accessories
属性 设置为 UICellAccessory.OutlineDisclosureOptions(style: .header)
。有一个示例项目以及其他有用的示例 here。
我目前面临的问题是,在将新快照应用到我当前的数据源时,header、页脚和装饰视图不是 collection 视图子视图的一部分,可以注意到这是一种奇怪的闪烁。有人以前遇到过这个问题吗?
我通过以下方式更新数据源:
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections(Sections.allCases)
items.forEach { snapshot.appendItems([[=11=]], toSection: ItemSectionMapper.getSection(for: [=11=])) }
self.dataSource?.apply(snapshot)
编辑:
它似乎只发生在 iOS 14 台设备上。
编辑2: 这是示例项目中同一问题的屏幕录像: https://imgur.com/a/0rS9aZU
下面的代码:
import UIKit
// MARK: - Cell -
final class Cell: UICollectionViewCell {
static let reuseIdentifier = "Cell"
var isExpanded = false {
didSet { label.numberOfLines = numberOfLines }
}
var numberOfLines: Int { isExpanded ? 0 : 3 }
lazy var label: UILabel = {
let label = UILabel()
label.numberOfLines = numberOfLines
label.frame.size = contentView.bounds.size
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(label)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
label.sizeThatFits(size)
}
}
final class Header: UICollectionReusableView {
static let elementKind = "Header"
lazy var label: UILabel = {
let label = UILabel()
label.numberOfLines = 1
label.frame.size = bounds.size
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(label)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
label.sizeThatFits(size)
}
}
// MARK: - UIViewController -
class ViewController: UIViewController {
struct Item: Hashable {
let text: String
var isExpanded = false
private let uuid = UUID()
}
var items: [Item] = [
.init(
text: """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore
eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident.
"""
),
.init(
text: """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore
eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident.
""",
isExpanded: true
)
]
lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createCollectionViewLayout())
collectionView.register(Cell.self, forCellWithReuseIdentifier: Cell.reuseIdentifier)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.contentInset.top = 44
collectionView.backgroundColor = .white
collectionView.delegate = self
return collectionView
}()
lazy var dataSource = UICollectionViewDiffableDataSource<Int, Item>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.reuseIdentifier, for: indexPath) as? Cell else { fatalError() }
cell.isExpanded = itemIdentifier.isExpanded
cell.label.text = itemIdentifier.text
return cell
}
override func viewDidLoad() {
super.viewDidLoad()
dataSource.supplementaryViewProvider = { (collectionView, kind, indexPath) -> UICollectionReusableView? in
guard let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: Header.elementKind, for: indexPath) as? Header else { fatalError() }
view.label.text = "Test"
return view
}
view.addSubview(collectionView)
collectionView.register(Header.self, forSupplementaryViewOfKind: Header.elementKind, withReuseIdentifier: Header.elementKind)
updateSnapshot()
}
private func createCollectionViewLayout() -> UICollectionViewCompositionalLayout {
let layoutSize = NSCollectionLayoutSize.init(
widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(200)
)
let section = NSCollectionLayoutSection(group:
.vertical(
layoutSize: layoutSize,
subitems: [.init(layoutSize: layoutSize)]
)
)
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .estimated(20)), elementKind: Header.elementKind, alignment: .top)
section.boundarySupplementaryItems = [header]
section.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
section.interGroupSpacing = 20
return .init(section: section)
}
private func updateSnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<Int, Item>()
snapshot.appendSections([0])
snapshot.appendItems(items)
dataSource.apply(snapshot, animatingDifferences: true)
}
}
// MARK: - UICollectionViewDelegate -
extension ViewController: UICollectionViewDelegate {
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let itemIdentifier = dataSource.itemIdentifier(for: indexPath) else { return }
items[indexPath.row] = .init(text: itemIdentifier.text, isExpanded: !itemIdentifier.isExpanded)
updateSnapshot()
}
}
感谢@JWK
这种行为似乎出乎意料,但我相信它的发生仅仅是因为整个部分在 apply(_:animatingDifferences:completion:)
中更新了动画。您可以尝试一些解决方法:
- 调用
apply(_:animatingDifferences:completion:)
时将animatingDifferences
设置为false
。如果你想要动画就不理想了。 - 添加另一个部分而不是使用
boundarySupplementaryItems
。不展开的部分不应在视觉上受到影响。您可能需要引入另一个单元格并为此使用UICollectionViewCompositionalLayout
的init(sectionProvider:)
(为每个部分提供正确的NSCollectionLayoutSection
)。 - 如果您使用的是 iOS 14+,我认为您可以通过将
UICollectionViewListCell
的accessories
属性 设置为UICellAccessory.OutlineDisclosureOptions(style: .header)
。有一个示例项目以及其他有用的示例 here。