我有一个集合视图,每个单元格都有一个“空星”按钮。当用户点击该按钮时,按钮的图像应该变成“实心星星”。 CollectionView 应该对自身进行排序,以便具有“填充星”的单元格位于第一个,并且任何先前具有“填充星”的单元格现在应该具有“空星”图像。
我的问题是,当用户点击另一个具有“空星”图像的单元格时,它确实会更新该单元格上的图像并进行排序,但前一个单元格不会更新(即它仍然显示一个实心星形而不是比一颗空星)。任何人都可以看看发生了什么吗?我发布了一个最小的可重现示例(这是一个您应该能够限制到视图控制器的视图)。
protocol ObjectCollectionViewDelegate {
func refreshStarred(object: Object)
}
class ObjectCollectionView: UIView, UICollectionViewDelegate {
var objects = [Object]()
var starredObject: Object?
lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: createCollectionViewLayout())
var dataSource: UICollectionViewDiffableDataSource<Int, Object>?
init() {
super.init(frame: .zero)
self.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
collectionView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
collectionView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
collectionView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.75).isActive = true
setUpDataSource()
collectionView.register(Cell.self, forCellWithReuseIdentifier: String(describing: Cell.self))
collectionView.delegate = self
self.objects = [Object(name: "First"), Object(name: "Second"), Object(name: "Third"), Object(name: "Fourth")]
self.refreshSnapshot()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// Called via lazy initialization of collectionview
private func createCollectionViewLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(0.5))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 10)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
return UICollectionViewCompositionalLayout(section: section)
}
private func setUpDataSource() {
dataSource = UICollectionViewDiffableDataSource<Int, Object>(collectionView: collectionView) {
(collectionView, indexPath, object) -> UICollectionViewCell? in
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: String(describing: Cell.self),
for: indexPath) as? Cell else {
fatalError("ObjectPicker -Could not cast cell as \(Cell.self)")
}
cell.delegate = self
cell.object = object
return cell
}
collectionView.dataSource = dataSource
var snapshot = NSDiffableDataSourceSnapshot<Int, Object>()
snapshot.appendSections([0])
dataSource?.apply(snapshot)
}
}
extension ObjectCollectionView: ObjectCollectionViewDelegate {
func refreshStarred(object: Object) {
// Unstar the previously starred object (if any)
if let previouslyStarredObject = starredObject {
previouslyStarredObject.isStarredObject = false
}
// Star the newly tapped object
object.isStarredObject = true
starredObject = object
self.refreshSnapshot()
}
func refreshSnapshot() {
guard let dataSource = dataSource else { return }
var objectsToRefreshWith = [Object]()
if self.objects.count == 0 {
self.starredObject = nil
} else if self.objects.count == 1 {
objectsToRefreshWith.append(objects[0])
objects[0].isStarredObject = true
starredObject = objects[0]
} else if self.starredObject == nil && objects.count > 0 {
objects[0].isStarredObject = true
starredObject = objects[0]
for object in objects {
if object != starredObject {
object.isStarredObject = false
}
objectsToRefreshWith.append(object)
}
} else if self.starredObject != nil {
guard let primary = objects.first(where: { $0.isStarredObject }) else {
print("Object Picker - the primary Object specified is not in the objects array")
return
}
objectsToRefreshWith.append(primary)
for object in objects where object != primary {
if object != starredObject {
object.isStarredObject = false
}
objectsToRefreshWith.append(object)
}
}
var snapShot = dataSource.snapshot(for: 0)
snapShot.applyDifferences(newItems: objectsToRefreshWith)
dataSource.apply(snapShot, to: 0, animatingDifferences: true)
}
}
class Cell: UICollectionViewCell {
var delegate: ObjectCollectionViewDelegate?
var object: Object? {
didSet {
if let object = object {
self.label.text = object.name
}
}
}
var button = UIButton()
let label = UILabel()
var imageConfig = UIImage.SymbolConfiguration(pointSize: 24)
var isStarredObject: Bool {
get { return object?.isStarredObject ?? false }
set { object?.isStarredObject = newValue }
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.init(white: 0.233, alpha: 1)
self.layer.cornerRadius = 20
self.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
button.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
button.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
button.bottomAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
self.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
label.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
label.topAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
label.textColor = .white
label.textAlignment = .center
let image = UIImage(systemName: "star", withConfiguration: imageConfig)
button.addTarget(self, action: #selector(buttonTouched), for: .allTouchEvents)
button.setImage(image, for: .normal)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateButton() {
guard let delegate = delegate else {
print("No object or delegate assigned to cell")
return
}
if isStarredObject {
let newImage = UIImage(systemName: "star.fill", withConfiguration: imageConfig)
self.button.setImage(newImage, for: .normal)
self.button.isEnabled = false
} else {
let newImage = UIImage(systemName: "star", withConfiguration: imageConfig)
self.button.setImage(newImage, for: .normal)
self.button.isEnabled = true
}
}
@objc func buttonTouched() {
guard let object = object else {
print("No object or delegate assigned to cell")
return
}
self.delegate?.refreshStarred(object: object)
self.updateButton()
}
}
class Object: Codable {
var name: String
var isStarredObject: Bool = false
init(name: String) {
self.name = name
}
}
extension Object: Hashable {
static func == (lhs: Object, rhs: Object) -> Bool {
return lhs.name == rhs.name
}
func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
}
extension NSDiffableDataSourceSectionSnapshot {
mutating func applyDifferences(newItems: [ItemIdentifierType]) {
guard self.items != newItems else { return }
let differences = newItems.difference(from: self.items)
for difference in differences {
switch difference {
case let .insert(offset: offset, element: item, _):
if self.items.isEmpty {
self.append([item])
} else if offset == 0 {
self.insert([item], before: self.items[0])
} else {
self.insert([item], after: self.items[offset - 1])
}
case let .remove(offset: _, element: item, _):
self.delete([item])
}
}
}
}
我解决了这个问题。即使我正在更新“对象”,并且数据源指示 CollectionView 更新单元格的position,但它并没有更新单元格的content。
我需要做的是检索每个单元格的索引并手动强制它更新内容。因此,看看上面的代码,在函数“refreshSnapshot”中,就在我定义数据源的位置下方,我添加了这一行:
var snapShot = dataSource.snapshot(for: 0)
for eachObject in objectsToRefreshWith {
guard let index = snapShot.index(of: eachObject), let cell = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) as? Cell else {
print("RefreshSnapshot - Could not get cell for \(eachObject)")
continue
}
cell.updateButton()
}
snapShot.applyDifferences(newItems: objectsToRefreshWith)
dataSource.apply(snapShot, to: 0, animatingDifferences: true)
我知道如果我只针对单个单元格,我不需要迭代每个对象,但是当从其他源加载数据时也会使用此函数,而且我不一定知道如果加载的对象将“isStarred”属性设置为 true。
如果有人能想到更有效的解决方案,我很高兴听到!