Search code examples
iosswiftuikit

Collection View cells affect each other


I have placed a toggle button in the collection view cells. I want to check each cell, but when I do this in a cell, when I do this in a cell, there is a check in other cells, when I scroll up and down, the cell I previously checked becomes uncheck. I did some operations using Notification Center to prevent this, but now when the number of data exceeds 15, the same distortion occurs when the cells affect each other. I really need your help! Can you help me with this?

Thank you very much.

I am adding the test codes below.

struct Item {
    var title: String
        var isSelected: Bool
        
        init(title: String, isSelected: Bool) {
            self.title = title
            self.isSelected = isSelected
        }
}

class CustomCollectionViewCell: UICollectionViewCell {
    
    var customCellObjects = CustomCellObjects()


    fileprivate func setupViews() {
        addSubview(customCellObjects)
        customCellObjects.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            customCellObjects.centerYAnchor.constraint(equalTo: self.centerYAnchor),
            customCellObjects.centerXAnchor.constraint(equalTo: self.centerXAnchor),
            customCellObjects.widthAnchor.constraint(equalTo: self.widthAnchor),
            customCellObjects.heightAnchor.constraint(equalTo: self.heightAnchor),
        ])
        
        customCellObjects.checkmarkButton.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
    }
    
    var isButtonSelected = false
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .systemRed
        setupViews()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configure(with item: Item) {
        NotificationCenter.default.post(name: NSNotification.Name("ToggleStateChanged"), object: self)
    }
    
    @objc func buttonTapped() {
        isButtonSelected.toggle()
        updateButtonAppearance()
    }
    
    private func updateButtonAppearance() {
        if isButtonSelected {
            customCellObjects.checkmarkButton.setImage(UIImage(named: "check"), for: .normal)
        } else {
            customCellObjects.checkmarkButton.setImage(nil, for: .normal)
        }
    }
    
}


class TestViewController: UIViewController {
    
    let cellIdd = "cellIdd"
    var testViewObjects = TestViewObjects()
    
    var items: [Item] {
        return [
            Item(title: "Item 1", isSelected: false),
            Item(title: "Item 2", isSelected: false),
            Item(title: "Item 3", isSelected: false),
            Item(title: "Item 4", isSelected: false),
            Item(title: "Item 5", isSelected: false),
            Item(title: "Item 6", isSelected: false),
            Item(title: "Item 7", isSelected: false),
            Item(title: "Item 8", isSelected: false),
            Item(title: "Item 9", isSelected: false),
            Item(title: "Item 10", isSelected: false),
            Item(title: "Item 11", isSelected: false),
            Item(title: "Item 12", isSelected: false),
            Item(title: "Item 13", isSelected: false),
            Item(title: "Item 14", isSelected: false),
            Item(title: "Item 15", isSelected: false),
            Item(title: "Item 16", isSelected: false),
        ]
    }
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        NotificationCenter.default.addObserver(self, selector: #selector(handleToggleStateChanged(_:)), name: NSNotification.Name("ToggleStateChanged"), object: nil)

        setupViews()
        collectionViewSetup()
    }
    
    fileprivate func setupViews() {
        view.addSubview(testViewObjects.collectionView)
        testViewObjects.collectionView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            testViewObjects.collectionView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0),
            testViewObjects.collectionView.widthAnchor.constraint(equalTo: view.widthAnchor),
            testViewObjects.collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
        
    @objc func handleToggleStateChanged(_ notification: Notification) {
        if let cell = notification.object as? CustomCollectionViewCell,
            let indexPath = testViewObjects.collectionView.indexPath(for: cell) {
            items[indexPath.item]
            testViewObjects.collectionView.reloadItems(at: [indexPath])
        }
    }
    
    fileprivate func collectionViewSetup() {
        testViewObjects.collectionView.delegate = self
        testViewObjects.collectionView.dataSource = self
        testViewObjects.collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: "cellIdd")
    }
}

extension TestViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
    // UICollectionViewDataSource yöntemleri
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return items.count //10 // Örnek olarak 10 hücre olsun
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdd, for: indexPath) as! CustomCollectionViewCell

        let listData = items[indexPath.row]
        cell.customCellObjects.nameLabel.text = listData.title
        cell.configure(with: items[indexPath.item])
        return cell
    }

    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: view.frame.width * 0.9, height: 100)
    }
}


class CustomCellObjects: UIView {
    
    lazy var checkmarkButton: UIButton = {
        let btn = UIButton(type: .system)
        btn.translatesAutoresizingMaskIntoConstraints = false
        btn.backgroundColor = .white
        btn.layer.cornerRadius = UIScreen.main.bounds.width * 0.04
        btn.layer.borderWidth = 2
        btn.layer.borderColor = UIColor.green.cgColor
        return btn
    }()
    
    lazy var nameLabel: UILabel = {
        let lbl = UILabel()
        lbl.translatesAutoresizingMaskIntoConstraints = false
        lbl.text = "Test Item"
        
        lbl.layer.borderWidth = 1
        lbl.layer.borderColor = UIColor.black.cgColor
        return lbl
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        addSubview(checkmarkButton)
        addSubview(nameLabel)
        NSLayoutConstraint.activate([
            checkmarkButton.centerYAnchor.constraint(equalTo: self.centerYAnchor),
            checkmarkButton.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 15),
            checkmarkButton.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.08),
            checkmarkButton.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.3),
            
            nameLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: 0),
            nameLabel.centerXAnchor.constraint(equalTo: self.centerXAnchor, constant: 0),
            nameLabel.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.5),
            nameLabel.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.2)
        ])
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}


class TestViewObjects: UIView {
    
    let collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
        cv.translatesAutoresizingMaskIntoConstraints = false
        cv.backgroundColor = .white
        return cv
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    fileprivate func setupViews() {
        addSubview(collectionView)
        
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: self.topAnchor),
            collectionView.widthAnchor.constraint(equalTo: self.widthAnchor),
            collectionView.heightAnchor.constraint(equalTo: self.heightAnchor),
        ])
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}

Solution

  • Step 1.

    The error is contained within these 2 methods.

    func configure(with item: Item) {
        NotificationCenter.default.post(name: NSNotification.Name("ToggleStateChanged"), object: self)
    }
        
    @objc func buttonTapped() {
        isButtonSelected.toggle()
        updateButtonAppearance()
    }
    

    Firstly, the notification center call for data update should definitely be located in tap handler. The configuration method should consume the selected state from item to cell. So these methods should look like this:

    func configure(with item: Item) {
        isButtonSelected = item.isSelected
        updateButtonAppearance()
    }
        
    @objc func buttonTapped() {
        NotificationCenter.default.post(name: NSNotification.Name("ToggleStateChanged"), object: self)
    }
    

    Step 2.

    You should find the way to update isSelected property on item when sending notification. You almost did everything for it, but you don't toggle the model isSelected state.

    Your method implementation:

    @objc func handleToggleStateChanged(_ notification: Notification) {
        if let cell = notification.object as? CustomCollectionViewCell,
            let indexPath = testViewObjects.collectionView.indexPath(for: cell) {
            items[indexPath.item]
            testViewObjects.collectionView.reloadItems(at: [indexPath])
        }
    }
    

    You should update the item's state like following:

    @objc func handleToggleStateChanged(_ notification: Notification) {
        if let cell = notification.object as? CustomCollectionViewCell,
            let indexPath = testViewObjects.collectionView.indexPath(for: cell) {
            items[indexPath.item].isSelected.toggle()
            testViewObjects.collectionView.reloadItems(at: [indexPath])
        }
    }
    

    Hope that helps!