0

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
    }
}
Evelyn
  • 186
  • 1
  • 4
  • 25

2 Answers2

1

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

igrek
  • 1,415
  • 1
  • 12
  • 27
  • It works! but how would I be able to transition it if say that it had a class in it that controls the number of cells in the tableview, like say if the class was called Labels and it replaced the text for pick 1, 2, & 3, sorry for being a pain I just forgot to ask this in the question since im still completing my project but really appreciate all the help – Evelyn Oct 29 '19 at 19:56
  • because it works with the change I did up until I get to the func cellForRowAt at ***cell.update (with: model)*** & ***cell.delegate = self*** – Evelyn Oct 29 '19 at 19:58
  • If you want your cell to represent either first/second/third buttons selection and some labels, you should update your model. Enum no longer covers what you need so, create a struct that contains both enum and label. `struct CellModel { let buttonSelectionIdentity: ButtonSelectionIdentity let labels: Labels }` – igrek Oct 30 '19 at 10:01
  • Use this struct in the place of `elements`, in your controller: `private var elements: [CellModel] = []` – igrek Oct 30 '19 at 10:03
  • And update accordingly: viewDidLoad should populate the new struct, cell's update method should take `CellModel` and thats it. – igrek Oct 30 '19 at 10:04
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/201591/discussion-between-igrek-and-evelyn). – igrek Oct 30 '19 at 10:05
0

You are using dequeuereusablecell in your tableview. These cells are reusable as the name implies. Meaning that, when you are scrolling your tableview, certain UI aspects of the cells can behave similarly.

To prevent this, you always need an "else" statement.

sender.isSelected = true; do {
    self.lastSelectedButton.backgroundColor = UIColor.red
}

If you don't provide an else statement it is very likely that you will see another red colors in your another cells.

Eray Alparslan
  • 806
  • 7
  • 12
  • that is already inside my cell in the IBAciton func btnTypeSelected and it wasn't working... which is why I was trying to to put it in an objc func in the view controller to see if I got a better result – Evelyn Oct 29 '19 at 14:39