2

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.

2 Answers2

3

Create a protocol with delegations for the decrementation and incrementation of the stepper, then add a delegate property to your cell for it to be set in the cellForRowAt method.

protocol TableViewCellDelegate {
    func tableViewCell(_ cell: TableViewCell, didDecrementStepper: UIStepper)
    func tableViewCell(_ cell: TableViewCell, didIncrementStepper: UIStepper)
}

The bonus part that allows us to decide which delegate method to call is the private stepperValue. As we are keeping track of the stepper value with our own property, we can benefit from the different set events.

class TableViewCell: UITableViewCell {

    // MARK: Outlets

    @IBOutlet private weak var stepper: UIStepper!

    // MARK: Properties

    weak var delegate: TableViewCellDelegate?

    private(set) var stepperValue: Double = 0 {
        didSet {
            if oldValue < stepperValue {
                delegate?.tableViewCell(self, didIncrementStepper: stepper)
            } else {
                delegate?.tableViewCell(self, didDecrementStepper: stepper)
            }
        }
    }

    // MARK: Actions

    @IBAction func didStepperValueChanged(_ sender: UIStepper) {

        stepperValue = sender.value
    }
}

We can use an @IBAction to connect the value changed event of the stepper to a method and update the value of our property. The didSet event uses the scope provided oldValue property to establish which way the value went.

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! TableViewCell
    cell.delegate = self
    return cell
}

Setting the controller as the delegate of the cell will require it to conform to the cell's protocol. You can create an extension to contain the delegate methods and access the table view.

extension TableViewController: TableViewCellDelegate {
    func tableViewCell(_ cell: TableViewCell, didIncrementStepper: UIStepper) {
        if let indexPath = tableView.indexPath(for: cell) {
            print(#function, indexPath)
        }
    }

    func tableViewCell(_ cell: TableViewCell, didDecrementStepper: UIStepper) {
        if let indexPath = tableView.indexPath(for: cell) {
            print(#function, indexPath)
        }
    }
}
Callam
  • 11,409
  • 2
  • 34
  • 32
  • 2
    The cell should not have any knowledge of its index path or table view. Remove those from the cell and the delegate protocol. The cell should simply pass itself to the delegate. The delegate can then do what it needs with the cell. – rmaddy Mar 13 '18 at 00:17
0

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
}
vadian
  • 274,689
  • 30
  • 353
  • 361
  • So far your option is my favourite, using this method, how could you tell if the stepper was incremented or decremented? Thanks – Yann Morin Charbonneau Mar 14 '18 at 01:16
  • Actually the old value of the stepper should be stored in the data model so you can compare both values to get the direction, however I updated the answer with a smart alternative to detect the step direction in the closure. – vadian Mar 14 '18 at 05:39
  • Awesome, I was able to use the NSKeyValueObservation to do exactly what I wanted to do. It works fine and it is easy to understand how the code you wrote does. Thank you! – Yann Morin Charbonneau Mar 14 '18 at 14:36