4

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.

Vicky Arora
  • 501
  • 2
  • 7
  • 20

3 Answers3

16

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

Abizern
  • 146,289
  • 39
  • 203
  • 257
  • This looks so much for your effort and I just gave an up vote for your effort. At this moment I am using Xcode 6.4. So I am not able to test the code. I am in-between development so don't want to download the Xcode 7 as it may require changes in the code. But if this works I will tick it as a correct answer once I have Xcode 7 running. Will update you very soon. Thanks – Vicky Arora Oct 05 '15 at 07:24
8

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.

Vicky Arora
  • 501
  • 2
  • 7
  • 20
  • The proper way to do this is to interact with the the model and have the cell display the updated values. There is no need to set a tag on a cell - which is a brittle solution. – Abizern Oct 04 '15 at 11:09
  • @Abizern Can you please put down your solution? I am still using the cell to display the values (just re declaring the cell inside the called function). But, I am not experienced enough to decode your answer in correct code form. I would really appreciate if you could put down your code. Me and many others would be thankful for your effort. – Vicky Arora Oct 04 '15 at 11:52
  • Add a picture of your cell and I'll make an example for you. – Abizern Oct 04 '15 at 11:56
  • I've added an answer with a link to an example that I wrote. – Abizern Oct 04 '15 at 13:20
  • your code does work! however, something weird happen for mine, when I scroll away from that cell and come back to it again, the label got "resetted" to its original state. Any idea why? – jo3birdtalk Feb 25 '16 at 04:35
2

Use tableView.reloadData() to reload your tableView content each time you click a button.

let text = "something"
 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 = something
    return cell
}

func addBtn(sender: AnyObject) -> Int {
    let button: UIButton = sender as! UIButton
    count = 1 + count
    println(count)
    something = "\(count)"
    self.tableView.reloadData()
    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)
    something = "\(count)"
    self.tableView.reloadData()
    return count
}

Update1

After your comments ... you have an array (one value for each food) like this, and whenever you click on a button, you take the index of the row the contains that button, then use that index to retrive the value of count from your array, then reload the table view content.

William Kinaan
  • 28,059
  • 20
  • 85
  • 118
  • Can you please tell me how to work with the button tag so that I can update only a particular cell label. With this code all the cell labels are updated. I just want to update the cell in which I press the buttons. Also, I am using animation to display the tableView. So everytime I reload the animation is shows. I don't want that action. Any other way around to update the label? – Vicky Arora Oct 04 '15 at 09:11
  • It seems you added your answer, do you still need help in this issue? – William Kinaan Oct 04 '15 at 09:56
  • Well, I did get the required functionality. But I am not sure if this is the right way. Do you have any better way to do this? Also, the only small issue I see in my solution is: I am using the variable "count" to store increased/decreased values. Once I change the count in Row 1 variable count is updated accordingly. Though only the selected row label count is updated, the variable count still has the Row 1 value when I try to change the row 2 value (the count starts from the Row 1 count value). It would be nice to reset the count as soon as I click another row. – Vicky Arora Oct 04 '15 at 10:14
  • in your solution what is the `cell` variable? plus, please tell me your scenario, do you have two rows ? which row do you want to update? does each row have a button to increase and a button to decrease ? – William Kinaan Oct 04 '15 at 10:17
  • let cell = tableView.cellForRowAtIndexPath(indexPath) as! FoodTypeTableViewCell! FoodTypeTableViewCell is the custom cell for FoodTypeViewController (which has the tableView). I have unique food item in each row and want to give the user the ability to increase/decrease the order size of a specific dish in each row. So yes, each row has its own two button (increase/decrease) and a lable to show the count. I want each row to start with a "0" count at first. Right now as the count variable retains the previous row value as soon as I click a button it starts from last count. – Vicky Arora Oct 04 '15 at 10:42
  • @VickyArora okay much better now, it is easy, but your way is NOT correct. I will give you a working example, but i need to know if the number of rows (foods) are static or dynamic. – William Kinaan Oct 04 '15 at 10:49
  • They are Dynamic. I upload them from the online database. – Vicky Arora Oct 04 '15 at 10:52
  • @VickyArora kindly did you check ? – William Kinaan Oct 04 '15 at 12:11
  • I didn't get you. Did you update the whole code or just the content below Update1 ? – Vicky Arora Oct 04 '15 at 12:21
  • the update1. I gave you the idea to implement your solution. it is pretty similar to your code, but you should have a variable for each row to save the number of counts for that food – William Kinaan Oct 04 '15 at 12:23