Search code examples
iosswiftuitableviewuistepper

Xcode, Swift - Get row Index of a button whiting a tableViewCell when its clicked


I am developing an app using Xcode in Swift. Briefly I have a tableViewController that houses many cells. There is a button in every cell (A stepper to be exact). My question is, how can I get the index of the cell which houses the stepper every time I click on it (the stepper)?

*Bonus point for knowing if the user pressed the 'Minus' or 'Plus' side of the stepper.

I search a very long time and I am still not sure how to approach this situation, nothing I try seems to be working.

Some context:

My tableViewController looks like this. enter image description here

Every cell has a few labels which are populated with informations taken from a file that I import. When importing the file I store all of its information in an array which I read every time I populate the tableView. Along with that, I have a label that will increment or decrement using the stepper. This allows me to track how much of which item I need to order. In the end, what I want to do is update my array with the amount I want to order so that it is saved for the next time I quit and reopen the app.

I have two ways that I think might work,

1: use the stepper to trigger a function that will increment or decrement the value for the order of its own cell using the index value for the cell in which the stepper is located.

2: Have the stepper only change the orderLabel, then have a save button that will go through every tableCell and read the orderLabel to save it into the array using the indexValue.

I don't know which would be the best way but I feel like the reading and the saving of the oderLabel to the array has to happen in my ItemTableViewController, as that is where I created the array which stores all my data.

Thanks, and I hope this will help.


Solution

  • In my humble opinion the most efficient way in Swift (and meanwhile even in Objective-C) is a callback closure / block.

    It's very easy to implement. The benefit is:

    • No extra protocol.
    • No extra delegate method.
    • The index path and the model item are directly available.

    In the UITableViewCell subclass declare a callback property with a closure (a Double parameter, no return value) and add an IBAction connected to the stepper. In the action call callback passing the value of the stepper:

    class MyTableViewCell: UITableViewCell {
    
        var callback : ((Double)->())?
    
        @IBAction func stepperChanged(_ sender: UIStepper) {
            callback?(sender.value)
        }
    }
    

    In the view controller in cellForRowAt assign a closure to the callback variable and handle the returned value. The model item as well as the index path is captured in the closure.

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "MyIdentifier", for: indexPath) as! MyTableViewCell
        let item = dataSourceArray[indexPath.row] // get the model item from the data source array
        //  update UI
        cell.callback = { value in
            print(indexPath, value)
            // do something with the new value for example update the model
        }
    
        return cell
    }
    

    In Swift 4 there is a smarter alternative, the new NSKeyValueObservation class. The benefit of this solution is that you can determine easily whether the stepper was incremented or decremented.

    Instead of the IBAction create an IBOutlet, connect it to the stepper and instead of the callback property declare a property of type NSKeyValueObservation

    class MyTableViewCell: UITableViewCell {
    
        @IBOutlet weak var stepper : UIStepper!
        var observation : NSKeyValueObservation?
    }
    

    In cellForRowAt assign the observer and pass the options old and new. In the closure compare oldValue with newValue to get the direction.

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "MyIdentifier", for: indexPath) as! MyTableViewCell
        let item = dataSourceArray[indexPath.row] // get the model item from the data source array
        //  update UI
        cell.observation = cell.stepper.observe(\.value, options: [.old, .new]) { (stepper, change) in
            let wasIncremented = change.oldValue! < change.newValue!
            print("new value:", change.newValue!, "stepper was incremented:", wasIncremented)
        }        
        return cell
    }