gpt4 book ai didi

ios - Collection View 可区分的数据源单元格消失且未正确调整大小?

转载 作者:行者123 更新时间:2023-12-03 20:54:02 28 4
gpt4 key购买 nike

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

Video of Error

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

collectionView.collectionViewLayout.invalidateLayout()

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

collectionView.reloadData()

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

集合 View Controller
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?()
}

}

邀请单元格 View 模型(单元格模型)
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
}

}

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

最佳答案

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

关于ios - Collection View 可区分的数据源单元格消失且未正确调整大小?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61570517/

28 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com