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?
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
}
}
H e llo, Evelyn! Use delegation pattern! :)
Here are some explanations to help you keep going:
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)
}
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.
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!
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