Vicky Arora Vicky Arora - 4 years ago 2525
Swift Question

iOS Swift, Update UITableView custom cell label outside of tableview CellForRow using tag

Setup (Swift 1.2 / iOS 8.4):

I have UITableView custom cell (identifier = Cell) inside UIViewController. Have two buttons (increment/decrement count) and a label (display count) inside the custom TableView cell.

Goal:

Update the label as we press the increase count or decrease count button.

At present I am able to get the button Tag and call a function outside of the CellForRowAtIndexPath. The button press increases and decreases the count. But I am not able to display the count update in the label.

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

let cell:FoodTypeTableViewCell = self.tableView!.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! FoodTypeTableViewCell

cell.addBtn.tag = indexPath.row // Button 1
cell.addBtn.addTarget(self, action: "addBtn:", forControlEvents: .TouchUpInside)

cell.subBtn.tag = indexPath.row // Button 2
cell.subBtn.addTarget(self, action: "subBtn:", forControlEvents: .TouchUpInside)

cell.countLabel.text = // How can I update this label
return cell
}

func addBtn(sender: AnyObject) -> Int {
let button: UIButton = sender as! UIButton
count = 1 + count
println(count)
return count
}

func subBtn(sender: AnyObject) -> Int {
let button: UIButton = sender as! UIButton
if count == 0 {
println("Count zero")
} else {
count = count - 1
}
println(count)
return count
}


I have seen this question here and there but was not able to find a clear answer in Swift. I would really appreciate if you could help answer it clearly so that other people can not just copy, but clearly understand what is going on.

Thank you.

Answer Source

Here is a solution that doesn't require tags. I'm not going to recreate the cell exactly as you want, but this covers the part you are asking about.

Using Swift 2 as I don't have Xcode 6.x anymore.

Let's start with the UITableViewCell subclass. This is just a dumb container for a label that has two buttons on it. The cell doesn't actually perform any specific button actions, it just passes on the call to closures that are provided in the configuration method. This is part of MVC. The view doesn't interact with the model, just the controller. And the controller provides the closures.

import UIKit

typealias ButtonHandler = (Cell) -> Void

class Cell: UITableViewCell {

    @IBOutlet private var label: UILabel!
    @IBOutlet private var addButton: UIButton!
    @IBOutlet private var subtractButton: UIButton!

    var incrementHandler: ButtonHandler?
    var decrementHandler: ButtonHandler?

    func configureWithValue(value: UInt, incrementHandler: ButtonHandler?, decrementHandler: ButtonHandler?) {
        label.text = String(value)
        self.incrementHandler = incrementHandler
        self.decrementHandler = decrementHandler
    }


    @IBAction func increment(sender: UIButton) {
        incrementHandler?(self)
    }


    @IBAction func decrement(sender: UIButton) {
        decrementHandler?(self)
    }
}

Now the controller is just as simple

import UIKit

class ViewController: UITableViewController {

    var data: [UInt] = Array(count: 20, repeatedValue: 0)

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return data.count
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! Cell

        cell.configureWithValue(data[indexPath.row], incrementHandler: incrementHandler(), decrementHandler: decrementHandler())

        return cell
    }

    private func incrementHandler() -> ButtonHandler {
        return { [unowned self] cell in
            guard let row = self.tableView.indexPathForCell(cell)?.row else { return }
            self.data[row] = self.data[row] + UInt(1)

            self.reloadCellAtRow(row)
        }
    }

    private func decrementHandler() -> ButtonHandler {
        return { [unowned self] cell in
            guard
                let row = self.tableView.indexPathForCell(cell)?.row
                where self.data[row] > 0
                else { return }
            self.data[row] = self.data[row] - UInt(1)

            self.reloadCellAtRow(row)
        }
    }

    private func reloadCellAtRow(row: Int) {
        let indexPath = NSIndexPath(forRow: row, inSection: 0)

        tableView.beginUpdates()
        tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
        tableView.endUpdates()
    }

}

When the cell is dequeued, it configures the cell with the value to show in the label and provides the closures that handle the button actions. These controllers are what interact with the model to increment and decrement the values that are being displayed. After changing the model, it reloads the changed cell in the tableview.

The closure methods take a single parameter, a reference to the cell, and from this it can find the row of the cell. This is a lot more de-coupled than using tags, which are a very brittle solution to knowing the index of a cell in a tableview.

You can download a full working example (Requires Xcode7) from https://bitbucket.org/abizern/so-32931731/get/ce31699d92a5.zip

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download