5

我的收藏视图有一个非常奇怪的问题。我正在使用适用于 iOS 13+ 的 Compositional Layout 和 Diffable Data Source API,但我遇到了一些非常奇怪的行为。如下面的视频所示,当我更新数据源时,添加到顶部的第一个单元格没有正确调整大小,然后当我添加第二个单元格时,两个单元格都消失了,然后当我添加第三个单元格时,都以适当的尺寸加载并出现。当我取消添加所有单元格并以类似的方式再次添加它们时,最初的问题不会再次发生。

错误视频

我尝试以某种方式使用以下解决方案:

collectionView.collectionViewLayout.invalidateLayout()

cell.contentView.setNeedsLayout() followed by cell.contentView.layoutIfNeeded()

collectionView.reloadData()

我似乎无法弄清楚可能导致此问题的原因。也许可能是我在集合视图中注册了两个不同的单元格,并且不正确地将它们出列,或者我的数据类型不正确地符合可散列。我相信我已经解决了这两个问题,但我也会提供我的代码来提供帮助。此外,提到的数据控制器是一个简单的类,它存储了一组视图模型,供单元格用于配置(那里不应该有任何问题)。谢谢!

集合视图控制器

import UIKit

class PartyInvitesViewController: UIViewController {

    private var collectionView: UICollectionView!

    private lazy var layout = createLayout()
    private lazy var dataSource = createDataSource()

    private let searchController = UISearchController(searchResultsController: nil)

    private let dataController = InvitesDataController()

    override func loadView() {
        super.loadView()

        collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)

        collectionView.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(collectionView)

        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let backButton = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
        backButton.tintColor = UIColor.Fiesta.primary
        navigationItem.backBarButtonItem = backButton

        let titleView = UILabel()
        titleView.text = "invite"
        titleView.textColor = .white
        titleView.font = UIFont.Fiesta.Black.header

        navigationItem.titleView = titleView

        navigationItem.searchController = searchController
        navigationItem.hidesSearchBarWhenScrolling = false
//        definesPresentationContext = true

        navigationItem.largeTitleDisplayMode = .never
        navigationController?.navigationBar.isTranslucent = true
        extendedLayoutIncludesOpaqueBars = true

        collectionView.register(InvitesCell.self, forCellWithReuseIdentifier: InvitesCell.reuseIdentifier)
        collectionView.register(InvitedCell.self, forCellWithReuseIdentifier: InvitedCell.reuseIdentifier)

        collectionView.register(InvitesSectionHeaderReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: InvitesSectionHeaderReusableView.reuseIdentifier)

        collectionView.delegate = self
        collectionView.dataSource = dataSource

        dataController.cellPressed = { [weak self] in
            self?.update()
        }

        dataController.start()

        update(animate: false)

        view.backgroundColor = .secondarySystemBackground
        collectionView.backgroundColor = .secondarySystemBackground
    }

}

extension PartyInvitesViewController: UICollectionViewDelegate {

    func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
//        cell.contentView.setNeedsLayout()
//        cell.contentView.layoutIfNeeded()
    }

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        if indexPath.section == InvitesSection.unselected.rawValue {
            let viewModel = dataController.getAll()[indexPath.item]
            dataController.didSelect(viewModel, completion: nil)
        }
    }

}

extension PartyInvitesViewController {

    func update(animate: Bool = true) {
        var snapshot = NSDiffableDataSourceSnapshot<InvitesSection, InvitesCellViewModel>()

        snapshot.appendSections(InvitesSection.allCases)
        snapshot.appendItems(dataController.getTopSelected(), toSection: .selected)
        snapshot.appendItems(dataController.getSelected(), toSection: .unselected)
        snapshot.appendItems(dataController.getUnselected(), toSection: .unselected)

        dataSource.apply(snapshot, animatingDifferences: animate) {
//            self.collectionView.reloadData()
//            self.collectionView.collectionViewLayout.invalidateLayout()
        }
    }

}

extension PartyInvitesViewController {

    private func createDataSource() -> InvitesCollectionViewDataSource {
        let dataSource = InvitesCollectionViewDataSource(collectionView: collectionView, cellProvider: { collectionView, indexPath, viewModel -> UICollectionViewCell? in

            switch indexPath.section {
            case InvitesSection.selected.rawValue:
                guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: InvitedCell.reuseIdentifier, for: indexPath) as? InvitedCell else { return nil }
                cell.configure(with: viewModel)
                cell.onDidCancel = { self.dataController.didSelect(viewModel, completion: nil) }
                return cell
            case InvitesSection.unselected.rawValue:
                guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: InvitesCell.reuseIdentifier, for: indexPath) as? InvitesCell else { return nil }
                cell.configure(with: viewModel)
                return cell
            default:
                return nil
            }

        })

        dataSource.supplementaryViewProvider = { collectionView, kind, indexPath -> UICollectionReusableView? in
            guard kind == UICollectionView.elementKindSectionHeader else { return nil }

            guard let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: InvitesSectionHeaderReusableView.reuseIdentifier, for: indexPath) as? InvitesSectionHeaderReusableView else { return nil }

            switch indexPath.section {
            case InvitesSection.selected.rawValue:
                view.titleLabel.text = "Inviting"
            case InvitesSection.unselected.rawValue:
                view.titleLabel.text = "Suggested"
            default: return nil
            }

            return view
        }

        return dataSource
    }

}

extension PartyInvitesViewController {

    private func createLayout() -> UICollectionViewLayout {
        let layout = UICollectionViewCompositionalLayout { section, _ -> NSCollectionLayoutSection? in
            switch section {
            case InvitesSection.selected.rawValue:
                return self.createSelectedSection()
            case InvitesSection.unselected.rawValue:
                return self.createUnselectedSection()
            default: return nil
            }
        }

        return layout
    }

    private func createSelectedSection() -> NSCollectionLayoutSection {
        let width: CGFloat = 120
        let height: CGFloat = 60

        let layoutSize = NSCollectionLayoutSize(widthDimension: .estimated(width), heightDimension: .absolute(height))

        let item = NSCollectionLayoutItem(layoutSize: layoutSize)
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: layoutSize, subitems: [item])

        let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(60))
        let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)

        let section = NSCollectionLayoutSection(group: group)
        section.boundarySupplementaryItems = [sectionHeader]
        section.orthogonalScrollingBehavior = .continuous
        // for some reason content insets breaks the estimation process idk why
        section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)
        section.interGroupSpacing = 20

        return section
    }

    private func createUnselectedSection() -> NSCollectionLayoutSection {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)

        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(60))
        let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])

        let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(60))
        let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)

        let section = NSCollectionLayoutSection(group: group)
        section.boundarySupplementaryItems = [sectionHeader]
        section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)
        section.interGroupSpacing = 20

        return section
    }

}

邀请单元格(第一个单元格类型)

class InvitesCell: FiestaGenericCell {

    static let reuseIdentifier = "InvitesCell"

    var stackView = UIStackView()
    var userStackView = UIStackView()
    var userImageView = UIImageView()
    var nameStackView = UIStackView()
    var usernameLabel = UILabel()
    var nameLabel = UILabel()
    var inviteButton = UIButton()

    override func layoutSubviews() {
        super.layoutSubviews()
        userImageView.layer.cornerRadius = 28
    }

    override func arrangeSubviews() {
        stackView.translatesAutoresizingMaskIntoConstraints = false

        contentView.addSubview(stackView)

        stackView.addArrangedSubview(userStackView)
        stackView.addArrangedSubview(inviteButton)

        userStackView.addArrangedSubview(userImageView)
        userStackView.addArrangedSubview(nameStackView)

        nameStackView.addArrangedSubview(usernameLabel)
        nameStackView.addArrangedSubview(nameLabel)

        setNeedsUpdateConstraints()
    }

    override func loadConstraints() {
        // Stack view constraints
        NSLayoutConstraint.activate([
            stackView.widthAnchor.constraint(equalTo: contentView.widthAnchor),
            stackView.heightAnchor.constraint(equalTo: contentView.heightAnchor)
        ])

        // User image view constraints
        NSLayoutConstraint.activate([
            userImageView.heightAnchor.constraint(equalToConstant: 56),
            userImageView.widthAnchor.constraint(equalToConstant: 56)
        ])
    }

    override func configureSubviews() {
        // Stack view configuration
        stackView.axis = .horizontal
        stackView.alignment = .center
        stackView.distribution = .equalSpacing

        // User stack view configuration
        userStackView.axis = .horizontal
        userStackView.alignment = .center
        userStackView.spacing = Constants.inset

        // User image view configuration
        userImageView.image = UIImage(named: "Image-4")
        userImageView.contentMode = .scaleAspectFill
        userImageView.clipsToBounds = true

        // Name stack view configuration
        nameStackView.axis = .vertical
        nameStackView.alignment = .leading
        nameStackView.spacing = 4
        nameStackView.distribution = .fillProportionally

        // Username label configuration
        usernameLabel.textColor = .white
        usernameLabel.font = UIFont.Fiesta.Black.text

        // Name label configuration
        nameLabel.textColor = .white
        nameLabel.font = UIFont.Fiesta.Light.footnote

        // Invite button configuration
        let configuration = UIImage.SymbolConfiguration(weight: .heavy)

        inviteButton.setImage(UIImage(systemName: "circle", withConfiguration: configuration), for: .normal)
        inviteButton.tintColor = .white
    }

}

extension InvitesCell {

    func configure(with viewModel: InvitesCellViewModel) {
        usernameLabel.text = viewModel.username
        nameLabel.text = viewModel.name

        let configuration = UIImage.SymbolConfiguration(weight: .heavy)

        if viewModel.isSelected {
            inviteButton.setImage(UIImage(systemName: "checkmark.circle.fill", withConfiguration: configuration), for: .normal)
            inviteButton.tintColor = .green
        } else {
            inviteButton.setImage(UIImage(systemName: "circle", withConfiguration: configuration), for: .normal)
            inviteButton.tintColor = .white
        }
    }

}

受邀细胞(第二细胞类型)

import UIKit

class InvitedCell: FiestaGenericCell {

    static let reuseIdentifier = "InvitedCell"

    var mainView = UIView()
    var usernameLabel = UILabel()
//    var cancelButton = UIButton()

    var onDidCancel: (() -> Void)?

    override func layoutSubviews() {
        super.layoutSubviews()
        mainView.layer.cornerRadius = 8
    }

    override func arrangeSubviews() {
        mainView.translatesAutoresizingMaskIntoConstraints = false
        usernameLabel.translatesAutoresizingMaskIntoConstraints = false

        contentView.addSubview(mainView)

        mainView.addSubview(usernameLabel)
    }

    override func loadConstraints() {
        // Main view constraints
        NSLayoutConstraint.activate([
            mainView.widthAnchor.constraint(equalTo: contentView.widthAnchor),
            mainView.heightAnchor.constraint(equalTo: contentView.heightAnchor)
        ])

        // Username label constraints
        NSLayoutConstraint.activate([
            usernameLabel.topAnchor.constraint(equalTo: mainView.topAnchor, constant: 20),
            usernameLabel.leftAnchor.constraint(equalTo: mainView.leftAnchor, constant: 20),
            usernameLabel.rightAnchor.constraint(equalTo: mainView.rightAnchor, constant: -20),
            usernameLabel.bottomAnchor.constraint(equalTo: mainView.bottomAnchor, constant: -20)
        ])
    }

    override func configureSubviews() {
        // Main view configuration
        mainView.backgroundColor = .tertiarySystemBackground

        // Username label configuration
        usernameLabel.textColor = .white
        usernameLabel.font = UIFont.Fiesta.Black.text
    }

}

extension InvitedCell {

    func configure(with viewModel: InvitesCellViewModel) {
        usernameLabel.text = viewModel.username
    }

    @objc func cancel() {
        onDidCancel?()
    }

}

Invites Cell View Model(细胞模型)

import Foundation

struct InvitesCellViewModel {

    var id = UUID()

    private var model: User

    init(_ model: User, selected: Bool) {
        self.model = model
        self.isSelected = selected
    }

    var username: String?
    var name: String?
    var isSelected: Bool

    mutating func toggleIsSelected() {
        isSelected = !isSelected
    }
}

extension InvitesCellViewModel: Hashable {

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(isSelected)
    }

    static func == (lhs: InvitesCellViewModel, rhs: InvitesCellViewModel) -> Bool {
        lhs.id == rhs.id && lhs.isSelected == rhs.isSelected
    }

}

如果我需要提供其他任何东西来更好地帮助回答这个问题,请在评论中告诉我!

4

1 回答 1

0

这可能不是适合所有人的解决方案,但我最终完全切换到了 RxSwift。对于那些争论切换的人,我现在使用 RxDataSources 和 UICollectionViewCompositionalLayout 几乎没有问题(除了偶尔的一两个错误)。我知道这可能不是大多数人正在寻找的答案,但回头看,这个问题似乎已经到了苹果的尽头,所以我认为最好找到另一条路。如果有人找到比完全跳到 Rx 更简单的解决方案,请随时添加您的答案。

于 2021-03-26T09:46:52.043 回答