Collection View Diffable Data Source 单元格消失且未正确调整大小?
Collection View Diffable Data Source cells disappearing and not resizing properly?
我的 collection 视图有一个非常奇怪的问题。我正在为 iOS 13+ 使用 Compositional Layout 和 Diffable Data Source API,但我遇到了一些非常奇怪的行为。正如下面的视频所示,当我更新数据源时,添加到顶部的第一个单元格没有正确调整大小,然后当我添加第二个单元格时,两个单元格都消失了,然后当我添加第三个单元格时,全部以适当的尺寸加载并出现。当我取消添加所有单元格并再次以类似方式将它们添加回去时,最初的问题不会再次发生。
我尝试过以某种方式使用以下解决方案:
collectionView.collectionViewLayout.invalidateLayout()
cell.contentView.setNeedsLayout() followed by cell.contentView.layoutIfNeeded()
collectionView.reloadData()
我似乎无法弄清楚是什么导致了这个问题。可能是我在 collection 视图中注册了两个不同的单元格,并且不正确地使它们出队,或者我的数据类型没有正确地符合 hashable。我相信我已经解决了这两个问题,但我也会提供我的代码来提供帮助。此外,提到的数据控制器是一个简单的 class,它存储用于配置的单元格的视图模型数组(那里应该没有任何问题)。谢谢!
Collection 视图控制器
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
}
}
如果我需要提供任何其他信息以更好地帮助回答这个问题,请在评论中告诉我!
这可能不是适合所有人的解决方案,但我最终完全转向了 RxSwift。对于那些正在讨论转换的人,我现在使用 RxDataSources 和 UICollectionViewCompositionalLayout 几乎没有问题(除了偶尔出现的一两个错误)。我知道这可能不是大多数人正在寻找的答案,但回过头来看,这个问题似乎在 Apple 端,所以我认为最好另辟蹊径。如果有人找到了比完全跳到 Rx 更简单的解决方案,请随时添加您的答案。
我的 collection 视图有一个非常奇怪的问题。我正在为 iOS 13+ 使用 Compositional Layout 和 Diffable Data Source API,但我遇到了一些非常奇怪的行为。正如下面的视频所示,当我更新数据源时,添加到顶部的第一个单元格没有正确调整大小,然后当我添加第二个单元格时,两个单元格都消失了,然后当我添加第三个单元格时,全部以适当的尺寸加载并出现。当我取消添加所有单元格并再次以类似方式将它们添加回去时,最初的问题不会再次发生。
我尝试过以某种方式使用以下解决方案:
collectionView.collectionViewLayout.invalidateLayout()
cell.contentView.setNeedsLayout() followed by cell.contentView.layoutIfNeeded()
collectionView.reloadData()
我似乎无法弄清楚是什么导致了这个问题。可能是我在 collection 视图中注册了两个不同的单元格,并且不正确地使它们出队,或者我的数据类型没有正确地符合 hashable。我相信我已经解决了这两个问题,但我也会提供我的代码来提供帮助。此外,提到的数据控制器是一个简单的 class,它存储用于配置的单元格的视图模型数组(那里应该没有任何问题)。谢谢!
Collection 视图控制器
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
}
}
如果我需要提供任何其他信息以更好地帮助回答这个问题,请在评论中告诉我!
这可能不是适合所有人的解决方案,但我最终完全转向了 RxSwift。对于那些正在讨论转换的人,我现在使用 RxDataSources 和 UICollectionViewCompositionalLayout 几乎没有问题(除了偶尔出现的一两个错误)。我知道这可能不是大多数人正在寻找的答案,但回过头来看,这个问题似乎在 Apple 端,所以我认为最好另辟蹊径。如果有人找到了比完全跳到 Rx 更简单的解决方案,请随时添加您的答案。