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")
}
}
2
Answers
The problem is that you are not taking account of the simple fact that cells are not rows. Instead, cells are reused; one cell can appear in different rows of the table, and will do so when (as you say) you scroll up and down.
So, do not every merely modify a cell directly, because it can then be reused in another row position. You must also modify the model to say whether a particular row has its button in the ON (checked) state or the OFF state, and modify your
cellForRowAt:
implementation (and/or yourconfigure
implementation) to take account of that. The job of the cells is to reflect the model; the model is source of truth.Step 1.
The error is contained within these 2 methods.
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:
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:
You should update the item’s state like following:
Hope that helps!