Search code examples
iosswiftuitableviewuiswitch

UISwitch in custom UITableViewCell Reuse Issue


The issue is as follows: I have a tableview with a custom cell. That cell contains a label and a UISwitch. I have set the label.text value to an array, but the UISwitch is getting reused.

Example: If I toggle the switch in the first row, the 5th row gets enabled, and if I scroll it continues to reuse the cells and cause issue.

Video : https://vimeo.com/247906440

View Controller:

class ViewController: UIViewController {

    let array = ["One","Two","Three","Four","Five","Six","Seven","Eight","Nine","Ten"]

    @IBOutlet weak var tableView: UITableView!
    override func viewDidLoad() {
        super.viewDidLoad()

    }

}

extension ViewController: UITableViewDelegate, UITableViewDataSource {

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CustomTableViewCell
        cell.label.text = array[indexPath.row]
        return cell
    }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return array.count
    }
}

Custom Cell:

class CustomTableViewCell: UITableViewCell {

    @IBOutlet weak var label: UILabel!
    @IBOutlet weak var toggleSwitch: UISwitch!

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

}

I realize there isn't code trying to store this data because I haven't been successful. Any ideas would be helpful. The project currently uses the MVC model and I believe that is the answer but just need some help.


Solution

  • I would recommend to you create cellViewModel class and keep array of it instead of just string. You cellViewModel may look like,

    class CellViewModel {
     let title: String
     var isOn: Bool
    
    init(withText text: String, isOn: Bool = false /* you can keep is at by default false*/) {
        self.title = text
        self.isOn = isOn
    } 
    

    Now, build array of CellViewModel

    let array =["One","Two","Three","Four","Five","Six","Seven","Eight","Nine","Ten"]
    var cellViewModels = [CellViewModel]()
    for text in array {
        let cellViewModel = CellViewModel(withText: text)
        cellViewModels.append(cellViewModel)
    }
    

    Change your tableVieDelegate function to :

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CustomTableViewCell
        let cellViewModel = cellViewModels[indexPath.row]
        cell.label.text = cellViewModel.title
        cell.toggleSwitch.isOn = cellViewModel.isOn
        cell.delegate = self
        return cell
    }
    

    In you Custom Cell class, add this protocol :

    protocol CellActionDelegate: class {
        func didChangeSwitchStateOnCell(_ cell: CustomTableViewCell)
    }
    

    Add delegate as property in your custom cell,

    weak var delegate: CellActionDelegate?
    

    Also, on switch change, add this line,

    delegate?.didChangeSwitchStateOnCell(self)
    

    Now, your viewController should register and listen to this delegate : I have added line cellForRowAtIndexPath to register for delegates. To listen this delegate, add this function in your VC.

    func didChangeSwitchStateOnCell(_ cell: CustomTableViewCell) {
        let indexPath = tableView.indexPath(for: cell)
        cellViewModels[indexPath.row].isOn = cell.toggleSwitch.isOn
    }