ios UICollectionView布局怪异

nhaq1z21  于 2022-12-15  发布在  iOS
关注(0)|答案(1)|浏览(219)

我在显示集合视图单元格时遇到了布局问题。所有的用户界面都是通过编程完成的。下面是向下滚动时发生的情况(用户界面元素出现在左上角):

单元格的内容正在动态布局,无法修复此问题。内容应在显示前布局。有何建议?
代码:

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()
    }

为什么布局引擎的行为如此奇怪,如何修复它?

zf9nrax1

zf9nrax11#

很难调试布局问题。但我不认为你需要删除你的视图,像这样:

override func prepareForReuse() {
    super.prepareForReuse()
    middleView.removeArrangedSubview(titleLabel)
    middleView.removeArrangedSubview(descriptionLabel)
    middleView.removeArrangedSubview(imageContainer)
    titleLabel.removeFromSuperview()
    descriptionLabel.removeFromSuperview()
    imageContainer.removeFromSuperview()

这迫使不必要的布局步骤,这可能解释了为什么您的单元格每次都要重新布局。
相反,只需将字段的值设置为nill,就像对图像所做的那样,当调用updateUI()函数时,添加新项中的新值,此处不应更新约束,如果在初始setupUI()方法中正确定义了约束,则内容的intrinsic content size将发生变化,单元格应能够适应。
自动布局约束是线性方程,因此当变量更改时,布局引擎重新计算值后,它们应该能够适应。
这可能需要一些时间来得到它的权利。你可能不得不玩弄压缩阻力和内容拥抱优先级,以确保测试领域不会缩小,一个好的经验法则是尝试和简化您的约束尽可能多。对不起,不能更具体,因为它很难调试布局没有模拟器。
还有一点。
我怀疑这是根本原因。但一个潜在的优化要记住。
您似乎正在使用模型Item(符合hashable)作为diffable数据源的ItemIdentifierType。

UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>

这确实是许多教程所展示的,但它不再是推荐的方式。使用整个对象的散列会导致单元格在每次字段更改时被破坏和添加。
这在docs中进行了说明,并在WWDC 21中进行了解释
最好的方法是将模型/视图模型传递到单元格中,如果模型属性发生变化,则更新布局。有点像您已经在做的事情。合并在这方面很方便。这样,只有在向数据源添加或从数据源删除一个全新的模型时,才会破坏单元格。

相关问题