J. Doe
J. Doe

Reputation: 13043

Make sure only 1 cell has an active state in a UICollectionView

I have an UICollectionView in which I want only want 1 cell to be active. With active I mean: the last cell that has been clicked (or the very first cell when to collection view lays out). When a user clicks a non-active cell, I want to reset the old active cell to a non-active state. I am having trouble doing this. This is because visibleCells, a property of collection view, only returns the cells on screen but not the cells in memory. This is my current way to locate an active cell and reset the state to non active.

This scenario can happen, causing multiple active cells: A user scroll slightly down so that the current active cell is not visible anymore, taps on a random cell and scroll up. The problem is that the old active cell stays in memory, although it is not visible: cellForItemAt(_:) does not gets called for that cell. Bad news is that visibleCells also do not find the old active cell. How can I find it? The function willDisplay cell also does not work.

An example project can be cloned directly into xCode: https://github.com/Jasperav/CollectionViewActiveIndex.

This is the code in the example project:

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var collectionView: CollectionView!
    static var activeIndex = 0

    override func viewDidLoad() {
        super.viewDidLoad()
        collectionView.go()
    }
}

class Cell: UICollectionViewCell {
    @IBOutlet weak var button: MyButton!

}

class CollectionView: UICollectionView, UICollectionViewDelegate, UICollectionViewDataSource {
    func go() {
        delegate = self
        dataSource = self
    }

    func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return 1
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 500
    }

    internal func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! Cell
        if indexPath.row == ViewController.activeIndex {
            cell.button.setTitle("active", for: .normal)
        } else {
            cell.button.setTitle("not active", for: .normal)
        }
        cell.button.addTarget(self, action: #selector(touchUpInside(_:)), for: .touchUpInside)
        return cell
    }

    @objc private func touchUpInside(_ sender: UIButton){
        let hitPoint = sender.convert(CGPoint.zero, to: self)
        guard let indexPath = indexPathForItem(at: hitPoint), let cell = cellForItem(at: indexPath) as? Cell else { return }

        // This is the problem. It does not finds the current active cell
        // if it is just out of bounds. Because it is in memory, cellForItemAt: does not gets called
        if let oldCell = (visibleCells as! [Cell]).first(where: { $0.button.titleLabel!.text == "active" }) {
            oldCell.button.setTitle("not active", for: .normal)
        }

        cell.button.setTitle("active", for: .normal)

        ViewController.activeIndex = indexPath.row
    }

}

Upvotes: 0

Views: 125

Answers (2)

Enrique Bermúdez
Enrique Bermúdez

Reputation: 1760

You can use the isSelected property of the UIColectionViewCell. You can set an active layout to your cell if it is selected. The selection mechanism is implemented by default in the UIColectionViewCell. If you want to select/activate more than one cell you can set the property allowsMultipleSelection to true.

Basically this approach will look like this:

class ViewController: UIViewController {

    @IBOutlet weak var collectionView: CollectionView!

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView.go()
    }

    func activeIndex()->Int?{
        if let selectedItems = self.collectionView.indexPathsForSelectedItems {
            if selectedItems.count > 0{
                return selectedItems[0].row
            }
        }
        return nil
    }
}

class Cell: UICollectionViewCell {

    @IBOutlet weak var myLabel: UILabel!

    override var isSelected: Bool{
        didSet{
            if self.isSelected
            {
                myLabel.text = "active"
            }
            else
            {
                myLabel.text = "not active"
            }
        }
    }
}


class CollectionView: UICollectionView, UICollectionViewDelegate, UICollectionViewDataSource {
    func go() {
        delegate = self
        dataSource = self
    }

    func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return 1
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 500
    }

    internal func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! Cell
        return cell
    }
}

Upvotes: 0

Shehata Gamal
Shehata Gamal

Reputation: 100523

To recover from this glitch you can try in cellForRowAt

cell.button.tag = indexPath.row

when the button is clicked set

ViewController.activeIndex = sender.tag
self.reloadData()

Upvotes: 1

Related Questions