我在显示集合视图单元格时遇到了布局问题。所有的用户界面都是通过编程完成的。下面是向下滚动时发生的情况(用户界面元素出现在左上角):
单元格的内容正在动态布局,无法修复此问题。内容应在显示前布局。有何建议?
代码:
class CollectionView: UICollectionView {
// MARK: - Enums
enum Section {
case main
}
typealias Source = UICollectionViewDiffableDataSource<Section, Item>
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Item>
private var source: Source!
private var dataItems: [Item] {...}
init(items: [Item]) {
super.init(frame: .zero, collectionViewLayout: .init())
collectionViewLayout = UICollectionViewCompositionalLayout { section, env -> NSCollectionLayoutSection? in
return NSCollectionLayoutSection.list(using: UICollectionLayoutListConfiguration(appearance: .plain), layoutEnvironment: env)
let cellRegistration = UICollectionView.CellRegistration<Cell, Item> { [unowned self] cell, indexPath, item in
cell.item = item
...modifications..
}
source = UICollectionViewDiffableDataSource<Section, Item>(collectionView: self) {
collectionView, indexPath, identifier -> UICollectionViewCell? in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration,
for: indexPath,
item: identifier)
}
}
func setDataSource(animatingDifferences: Bool = true) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(dataItems, toSection: .main)
source.apply(snapshot, animatingDifferences: animatingDifferences)
}
}
//Cell
class Cell: UICollectionViewListCell {
public weak var item: Item! {
didSet {
guard !item.isNil else { return }
updateUI()
}
}
private lazy var headerView: UIStackView = {...nested UI setup...}()
private lazy var middleView: UIStackView = {...nested UI setup...}()
private lazy var bottomView: UIStackView = {...nested UI setup...}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
private func setupUI() {
let items = [
headerView,
middleView,
bottomView
]
items.forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
contentView.addSubviews(items)
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: topAnchor),
contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor),
headerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding),
headerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: padding),
headerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -padding),
middleView.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: padding),
middleView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: padding),
middleView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -padding),
bottomView.topAnchor.constraint(equalTo: middleView.bottomAnchor, constant: padding),
bottomView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: padding),
bottomView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -padding),
])
let constraint = bottomView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -padding)
constraint.priority = .defaultLow
constraint.isActive = true
}
@MainActor
func updateUI() {
func updateHeader() {
//...colors & other ui...
}
func updateMiddle() {
middleView.addArrangedSubview(titleLabel)
middleView.addArrangedSubview(descriptionLabel)
guard let constraint = titleLabel.getConstraint(identifier: "height"),
let constraint2 = descriptionLabel.getConstraint(identifier: "height")
else { return }
titleLabel.text = item.title
descriptionLabel.text = item.truncatedDescription
//Tried to force layout - didn't help
// middleView.setNeedsLayout()
//calc ptx height
constraint.constant = item.title.height(withConstrainedWidth: bounds.width, font: titleLabel.font)
//Media
if let media = item.media {
middleView.addArrangedSubview(imageContainer)
if let image = media.image {
imageView.image = image
} else if !media.imageURL.isNil {
guard let shimmer = imageContainer.getSubview(type: Shimmer.self) else { return }
shimmer.startShimmering()
Task { [weak self] in
guard let self = self else { return }
try await media.downloadImageAsync()
media.image.publisher
.sink {
self.imageView.image = $0
shimmer.stopShimmering()
}
.store(in: &self.subscriptions)
}
}
}
constraint2.constant = item.truncatedDescription.height(withConstrainedWidth: bounds.width, font: descriptionLabel.font)
// middleView.layoutIfNeeded()
}
func updateBottom() { //...colors & other ui... }
updateHeader()
updateMiddle()
updateBottom()
}
override func prepareForReuse() {
super.prepareForReuse()
//UI cleanup
middleView.removeArrangedSubview(titleLabel)
middleView.removeArrangedSubview(descriptionLabel)
middleView.removeArrangedSubview(imageContainer)
titleLabel.removeFromSuperview()
descriptionLabel.removeFromSuperview()
imageContainer.removeFromSuperview()
imageView.image = nil
}
}
尝试在UICollectionViewDelegate中强制布局,但没有帮助:
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
cell.setNeedsLayout()
cell.layoutIfNeeded()
}
为什么布局引擎的行为如此奇怪,如何修复它?
1条答案
按热度按时间zf9nrax11#
很难调试布局问题。但我不认为你需要删除你的视图,像这样:
这迫使不必要的布局步骤,这可能解释了为什么您的单元格每次都要重新布局。
相反,只需将字段的值设置为nill,就像对图像所做的那样,当调用updateUI()函数时,添加新项中的新值,此处不应更新约束,如果在初始setupUI()方法中正确定义了约束,则内容的intrinsic content size将发生变化,单元格应能够适应。
自动布局约束是线性方程,因此当变量更改时,布局引擎重新计算值后,它们应该能够适应。
这可能需要一些时间来得到它的权利。你可能不得不玩弄压缩阻力和内容拥抱优先级,以确保测试领域不会缩小,一个好的经验法则是尝试和简化您的约束尽可能多。对不起,不能更具体,因为它很难调试布局没有模拟器。
还有一点。
我怀疑这是根本原因。但一个潜在的优化要记住。
您似乎正在使用模型Item(符合hashable)作为diffable数据源的ItemIdentifierType。
这确实是许多教程所展示的,但它不再是推荐的方式。使用整个对象的散列会导致单元格在每次字段更改时被破坏和添加。
这在docs中进行了说明,并在WWDC 21中进行了解释
最好的方法是将模型/视图模型传递到单元格中,如果模型属性发生变化,则更新布局。有点像您已经在做的事情。合并在这方面很方便。这样,只有在向数据源添加或从数据源删除一个全新的模型时,才会破坏单元格。