UICollectionView 单元格在关联数据更新时不更新

问题描述 投票:0回答:1

我有一个集合视图,每个单元格都有一个“空星”按钮。当用户点击该按钮时,按钮的图像应该变成“实心星星”。 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])
            }
        }
    }
}
swift uicollectionview uikit uicollectionviewcell
1个回答
0
投票

我解决了这个问题。即使我正在更新“对象”,并且数据源指示 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。

如果有人能想到更有效的解决方案,我很高兴听到!

© www.soinside.com 2019 - 2024. All rights reserved.