Search code examples
iosswiftbuttoncolorscell

How to prevent cells from mirroring button pressed action in another cell?


I have 3 buttons in the cells of my tableview, they buttons are in a @IBAction button collection so when one is selected it turns the button color from blue to red and deselects the button previously pressed to back to blue. the code works fine when performing those actions

the problem that im having is that when a button is selected in one cell, the exact same button will be selected in another cell as shown below▼

so far what ive tried isn't working and I think what might work is that I to create an "@objc func" in the view controller but I dont know where to go from what ive created to prevent the "mirroring" in the cells

I know that Im close to the solution thank you in advance for any help that you give

How to update UILabel on a button click in UITableViewCell in swift 4 and xcode 9?

cell image

import UIKit

class Cell: UITableViewCell {

    @IBOutlet weak var lbl1: UILabel!
    @IBOutlet weak var lbl2: UILabel!
    @IBOutlet weak var lbl3: UILabel!

    @IBOutlet weak var btn1: RoundButton!
    @IBOutlet weak var btn2: RoundButton!
    @IBOutlet weak var btn3: RoundButton!

    var lastSelectedButton = UIButton()
    @IBAction func cartTypeSelected(_ sender: RoundButton) {
        lastSelectedButton.isSelected = false; do {
            self.lastSelectedButton.backgroundColor = UIcolor.blue
        } //Plus any deselect logic for this button
        lastSelectedButton = sender //If any buttons are not affect by this selection logic exclude them here
        sender.isSelected = true; do {
            self.lastSelectedButton.backgroundColor = UIColor.red
        }
    }
}

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!

    override func viewDidLoad() {
         super.viewDidLoad()

         tableView.dataSource = self
         tableView.delegate = self

    }
    var lastSelectedButton = UIButton()
    @objc func selectedButton(_ sender: RoundButton) {
        lastSelectedButton.isSelected = false; do {
            self.lastSelectedButton.backgroundColor = UIcolor.blue
        } //Plus any deselect logic for this button
        lastSelectedButton = sender 
        sender.isSelected = true; do {
            self.lastSelectedButton.backgroundColor = UIColor.red
        }
    }
}

extension View[![enter image description here][1]][1]Controller: UITableViewDelegate, UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 100
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as? Cell else { return UITableViewCell() }

        return cell
    }
}

Solution

  • H e llo, Evelyn! Use delegation pattern! :)

    Here are some explanations to help you keep going:

    1. create a model to represent your table view contents

    What kind of a model could we use, to represent the state of having only ever a single button selected? An enum can represent that(add it in a separate file or in your controller):

    enum ButtonSelectionIdentity {
        case first
        case second
        case third
    }
    

    Our table view is going to present an array of those enums, in the controller, lets add an instance variable to hold the data, and initialize it with an empty array:

    private var elements: [ButtonSelectionIdentity] = []
    

    Let's populate this array with 100 elements, defaulting to the first button being selected, in your controller viewDidLoad function, add:

        for i in 0..<100 {
            elements.append(ButtonSelectionIdentity.first)
        }
    
    1. make sure cells are updated from the model

    So now we have a model(an array of ButtonSelectionIdentity), and we want table view controller to reflect that model. To do so, we change the original way of how a controller conformed UITableViewDataSource. We need the new implementation to take the data from the array:

    extension ViewController: UITableViewDataSource {
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return elements.count
        }
    
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as? Cell else { 
                return UITableViewCell() 
            }
    
            let model = elements[indexPath.row]
            cell.update(with: model)
    
            return cell
        }
    }
    

    Yes, after above change it won't compile, until we add an update method to the cell class:

    func update(with model: ButtonSelectionIdentity) {
        btn1.backgroundColor = .blue
        btn2.backgroundColor = .blue
        btn3.backgroundColor = .blue
    
        switch model {
        case .first:
            btn1.backgroundColor = .red
        case .second:
            btn2.backgroundColor = .red
        case .third:
            btn3.backgroundColor = .red
        }
    }
    

    Compile and run, you should see 100 cells having the first square red.

    1. Lets wire up cells actions to controller

    Remove buttonSelected method in your controller class, and remove your btnTypeSelected method of your Cell class, so that we can start over.

    At this stage, we have an array of elements, that are presented on the table view, inside of the controller. Controller owns it, because it created it. Cells are there to only present the state that the controller has. So, in order to get our cell to update, we need to tell controller, that we are updating. To do that, we can use delegation pattern. Lets create a cell delegate protocol to describe it.

    In your Cell class file, before the class Cell ..., add:

    protocol CellDelegate: class {
        func onCellModelChange(cell: Cell, model: ButtonSelectionIdentity)
    }
    

    So this is the delegate we will use to let the controller know about the state change in the cell. Lets add a weak reference to the cell to the delegate. In your Cell, add:

    weak var delegate: CellDelegate?
    

    Now, conform your controller to the CellDelegate protocol. In your controller class, add:

    extension ViewController: CellDelegate {
        func onCellModelChange(cell: Cell, model: ButtonSelectionIdentity) {
        }
    }
    

    for now we will leave it empty, and will finish it later.

    Now, controller can be a delegate of a cell. Lets make it to be the one! Update the cellForRowAt method of your controller, as follows:

        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as? Cell else { 
                return UITableViewCell() 
            }
    
            let model = elements[indexPath.row]
            cell.update(with: model)
            cell.delegate = self
    
            return cell
        }
    

    Done, we configured our controller to be the cell's delegate! Let's make some use of that!

    1. Make controller update model, when cell reports it's state change

    In your cell, wire up IBActions on each of the buttons separately:

    @IBAction func onFirstButtonTapped(_ sender: RoundButton) {
    }
    
    @IBAction func onSecondButtonTapped(_ sender: RoundButton) {
    }
    
    @IBAction func onThirdButtonTapped(_ sender: RoundButton) {
    }
    

    Whenever a button is tapped, we want our cell to tell the controller of a state change, for example:

    @IBAction func onFirstButtonTapped(_ sender: RoundButton) {
        delegate?.onCellModelChange(cell: self, model: .first)
    }
    

    Implement the other two methods accordingly.

    In your controller, let's revisit onCellModelChange method. Now that an action on a cell happened, we need to find an element in the elements array, corresponding to that cell. To do that, we can make use of tableView-s -indexPath(for:) method:

    extension ViewController: CellDelegate {
        func onCellModelChange(cell: Cell, model: ButtonSelectionIdentity) {
            guard let indexPath = tableView.indexPath(for: cell) else {
                return
            }
            print(indexPath)
        }
    }
    

    If you run the app, at this stage you should see logs of the indexpaths corresponding to cells that you press the buttons on. Not quite what we need yet.

    Our table view is only presenting a single section, so we can ignore the section from the index path, and only consider a row, which will be the same as our element index. Lets update the value in the array, using this index:

    extension ViewController: CellDelegate {
        func onCellModelChange(cell: Cell, model: ButtonSelectionIdentity) {
            guard let indexPath = tableView.indexPath(for: cell) else {
                return
            }
            let index = indexPath.row
            elemets[index] = model
        }
    }
    

    Now if you run this, you should get the model updated, but the cell's state won't update right away. You can still see it working, if you scroll the cell out of screen, and scroll back again.

    The last bit is making the cell update right away. How can we do that? Lets just put the updated model back to the cell:

    extension ViewController: CellDelegate {
        func onCellModelChange(cell: Cell, model: ButtonSelectionIdentity) {
            guard let indexPath = tableView.indexPath(for: cell) else {
                return
            }
            let index = indexPath.row
            elemets[index] = model
            cell.update(with: model)
        }
    }
    

    And this should be it! I didn't test it and didn't compile it :) So if you find any typos, let me know :) Cheers