Ячейки Difable Data Source исчезают и не меняют размеры? - PullRequest
0 голосов
/ 03 мая 2020

У меня действительно странная проблема с моим представлением коллекции. Я использую API-интерфейсы Compositional Layout и Diffable Data Source для iOS 13+, но у меня действительно странное поведение. Как видно из видео ниже, когда я обновляю источник данных, первая ячейка, добавляемая в верхний раздел, не изменяется должным образом, затем, когда я добавляю вторую ячейку, обе ячейки исчезают, а затем, когда я добавляю третью ячейку, все загружаются с правильными размерами и появляются. Когда я удаляю все ячейки и добавляю их аналогичным образом во второй раз, эта первоначальная проблема больше не возникает.

Видео с ошибкой

Я пытался каким-то образом использовать следующие решения:

collectionView.collectionViewLayout.invalidateLayout()

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

collectionView.reloadData()

Не могу понять, что может быть причиной этой проблемы. Возможно, возможно, что у меня есть две разные ячейки, зарегистрированные в представлении коллекции и неправильно выписывающие их из очереди, или мои типы данных не соответствуют правильно hashable. Я считаю, что я исправил обе эти проблемы, но я также предоставлю свой код, чтобы помочь. Также упомянутый контроллер данных - это простой класс, который хранит массив моделей представлений для ячеек, используемых для конфигурации (там не должно быть никаких проблем). Спасибо!

Контроллер представления коллекции

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

}

Приглашает модель представления ячейки (модель для ячеек)

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
    }

}

Если мне нужно предоставить что-нибудь еще, чтобы лучше помочь в ответе на этот вопрос, пожалуйста, дайте мне знать в комментариях!

...