Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 953
Vicky Arora Avatar asked Nov 27 '22 20:11

Vicky Arora


2 Answers

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

like image 122
Abizern Avatar answered Feb 15 '23 23:02

Abizern


I have never seen anything like this before so I am not sure if this will be the correct way to do. But I got the intended functionality using the bellow code:

For people who find it difficult to understand: The only problem we have in this is to refer to the TableView Cell. Once you figure out a way to refer the cell, you can interact with the cell components.

func addBtn(sender: AnyObject) -> Int {
    let button: UIButton = sender as! UIButton

    let indexPath = NSIndexPath(forRow: sender.tag, inSection: 0) // This defines what indexPath is which is used later to define a cell
    let cell = tableView.cellForRowAtIndexPath(indexPath) as! FoodTypeTableViewCell! // This is where the magic happens - reference to the cell

    count = 1 + count
    println(count)
    cell.countLabel.text = "\(count)" // Once you have the reference to the cell, just use the traditional way of setting up the objects inside the cell.
    return count
}

func subBtn(sender: AnyObject) -> Int {
    let button: UIButton = sender as! UIButton

    let indexPath = NSIndexPath(forRow: sender.tag, inSection: 0)
    let cell = tableView.cellForRowAtIndexPath(indexPath) as! FoodTypeTableViewCell!

    if count == 0 {
        println("Count zero")
    } else {
        count = count - 1
    }

    cell.countLabel.text = "\(count)"
    println(count)
    return count
}

I hope someone will benefit from this.

PLEASE CORRECT ME IF THERE IS SOME PROBLEM IN THIS SOLUTION OR THERE IS A BETTER/PROPER WAY TO DO THIS.

like image 41
Vicky Arora Avatar answered Feb 16 '23 00:02

Vicky Arora