3

Another Swift beginner here. I simply want a Stepper in each of my TableView cells that increments a label in the same cell.

I have found a couple of questions on this topic, but they include other elements and I haven't been able to extract the basic concept.

Swift Stepper Action that changes UITextField and UILabel within same cell

Stepper on tableview cell (swift)

So far I have connected IBOutlets for my Label and Stepper, as well as an IBAction for my Stepper in my cell class.

class BuyStatsCell: UITableViewCell{

    //these are working fine

    @IBOutlet weak var category: UILabel!
    @IBOutlet weak var average: UILabel!
    @IBOutlet weak var price: UILabel!


    //Outlet for Label and Stepper - How do I make these work?

    @IBOutlet weak var purchaseAmount: UILabel!
    @IBOutlet weak var addSubtract: UIStepper!

    //Action for Stepper - And this?

    @IBAction func stepperAction(_ sender: UIStepper) {
        self.purchaseAmount.text = Int(sender.value).description

    }

}

And I understand the concept of reusing the cell in the cellForRowAt indexPath

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "BuyStatsTabCell", for: indexPath) as! BuyStatsCell

        cell.isUserInteractionEnabled = false

 //these are working 

        cell.category.text = categories[indexPath.row]
        cell.price.text = String(prices[indexPath.row])
        cell.average.text = String(averages[indexPath.row])

//but is there something I need to add here to keep the correct Stepper and Label for each class?

    return cell
}

One of the already asked questions includes a protocol and another function in the ViewController like this

protocol ReviewCellDelegate{
    func stepperButton(sender: ReviewTableViewCell)
}

func stepperButton(sender: ReviewTableViewCell) {
    if let indexPath = tableView.indexPathForCell(sender){
        print(indexPath)
    }
}

I don't know if this is the approach I should be trying to take. I am looking for the simplest solution, but I am having trouble putting the pieces together.

Any help is appreciated. Thanks.

Community
  • 1
  • 1
Louis Sankey
  • 481
  • 8
  • 26
  • Why don't you use a single source array with product models, and add price, average, and category as properties? Then when you change the models, and reload with the changed models it should be persistent. I'm not really sure if persistence is the problem. – silentBob Mar 18 '17 at 16:41
  • So, what are you trying to achieve is to let the stepper to increase/decrease the value of current object -based on the row- of `categories`, right? – Ahmad F Mar 18 '17 at 16:51
  • Thank you both for the reply. I added some comments to my code for clarification. Category, Average, and Price are all working fine. purchaseAmount is the Label I am trying to update with Stepper. Each row on my list has a purchaseAmount and a Stepper used to add or subtract. – Louis Sankey Mar 18 '17 at 17:07
  • As @vadian suggested, I'd recommend to let the array data source to be only one, containing models, rather than reading from `categories`, `prices` and `averages`. Perhaps checking [this Q&A](http://stackoverflow.com/questions/39738518/uitableview-filtering/39740482#39740482) would help you to understand it. – Ahmad F Mar 18 '17 at 17:33

2 Answers2

7

Easiest solution (simplyfied):

  • Create a model BuyStat with a property purchaseAmount (it's crucial to be a class).
    You are strongly discouraged from using multiple arrays as data source

    class BuyStat {
    
        var purchaseAmount = 0.0
    
        init(purchaseAmount : Double) {
            self.purchaseAmount = purchaseAmount
        }
    }
    
  • In the view controller create a data source array

    var stats = [BuyStat]()
    
  • In viewDidLoad create a few instances and reload the table view

    stats = [BuyStat(purchaseAmount: 12.0), BuyStat(purchaseAmount: 20.0)]
    tableView.reloadData()
    
  • In the custom cell create a property buyStat to hold the current data source item with an observer to update stepper and label when buyStat is set

    class BuyStatsCell: UITableViewCell {
    
        @IBOutlet weak var purchaseAmount: UILabel!
        @IBOutlet weak var addSubtract: UIStepper!
    
        var buyStat : BuyStat! {
            didSet {
                addSubtract.value = buyStat.purchaseAmount
                purchaseAmount.text = String(buyStat.purchaseAmount)
            }
        }
    
        @IBAction func stepperAction(_ sender: UIStepper) {
            buyStat.purchaseAmount = sender.value
            self.purchaseAmount.text = String(sender.value)
        }
    }
    
  • In cellForRowAtIndexPath get the data source item and pass it to the cell

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "BuyStatsTabCell", for: indexPath) as! BuyStatsCell
        cell.buyStat = stats[indexPath.row]
        return cell
    }
    

The magic is: When you are tapping the stepper the label as well as the data source array will be updated. So even after scrolling the cell will get always the actual data.

With this way you don't need protocols or callback closures. It's only important that the model is a class to have reference type semantics.

vadian
  • 274,689
  • 30
  • 353
  • 361
  • I think this approach is better than handling the action of the stepper at the ViewController layer, by recognizing the current stepper -via adding a tag for each one-, editing the value of array data source and calling `reloadData()`, I find it more decoupled and logical, is this right? :) – Ahmad F Mar 18 '17 at 17:28
  • Thank you for this excellent answer. I am confident I will be able to implement based on this. So when you say 'stats = [BuyStat(purchaseAmount: 12.0), BuyStat(purchaseAmount: 20.0)] tableView.reloadData()' is this to create two list rows with initial values 12 and 20 for purchaseAmount? Then use the Stepper to increase or decrease that amount? – Louis Sankey Mar 18 '17 at 17:31
  • It is, the most significant benefit is the reference to the data source item, so view and model are updated simultaneously. @ithinkthereforeIprogr'am' Yes, exactly. – vadian Mar 18 '17 at 17:31
  • I'm still having trouble getting the Stepper to be responsive at all. Don't know what I'm doing wrong as I have tried reconnecting the Stepper outlet and action multiple times. :-/ I put a print statement in my IBAction but its not working. Heres a couple screenshots https://i.stack.imgur.com/zpnvt.png https://i.stack.imgur.com/J6p7x.png – Louis Sankey Mar 18 '17 at 22:36
  • What if I have default value like `priceLabel.text = 15` and want to increase this value with self for each click to stepper?(e.g. 2 clicks assign to 15+15 = 30) – Emre Değirmenci Mar 06 '19 at 18:37
  • @EmreDeğirmenci Adjust `stepValue` in the stepper instance. – vadian Mar 06 '19 at 18:48
  • 1
    @EmreDeğirmenci In Interface Builder select the stepper, press ⌥⌘4 to open the Attributes Inspector and set *Step* to 15 – vadian Mar 06 '19 at 20:25
  • @EmreDeğirmenci Please ask a new question. – vadian Mar 07 '19 at 10:43
  • I didn't need to create wrapper data model. Instead I used an Int array and created an IBAction reference to the stepper in my ViewController and call reloadData every time. When generating the cells, I update my array containing the stepper quantities based on the value in the label for that cell – btrballin Apr 23 '19 at 04:50
0

NOTE: MY Cell class is just normal..All changes are in viewcontroller class

class cell: UITableViewCell {

    @IBOutlet weak var ibAddButton: UIButton!
    @IBOutlet weak var ibStepper: UIStepper!
    @IBOutlet weak var ibCount: UILabel!
    @IBOutlet weak var ibLbl: UILabel!
}

1.define empty int array [Int]()

var countArray = [Int]()

2.append countArray with all zeros with the number of data u want to populate in tableview

 for arr in self.responseArray{
        self.countArray.append(0)
   }

3.in cell for row at

func tableView(_ tableView: UITableView, cellForRowAt indexPath:
 IndexPath) -> UITableViewCell {

         let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! cell
         let dict = responseArray[indexPath.row] as? NSDictionary ?? NSDictionary()
         cell.ibLbl.text = dict["name"] as? String ?? String()
         if countArray[indexPath.row] == 0{
             cell.ibAddButton.tag = indexPath.row
             cell.ibStepper.isHidden = true
             cell.ibAddButton.isHidden = false
             cell.ibCount.isHidden = true
             cell.ibAddButton.addTarget(self, action: #selector(addPressed(sender:)), for: .touchUpInside)
         }else{
             cell.ibAddButton.isHidden = true
             cell.ibStepper.isHidden = false
             cell.ibStepper.tag = indexPath.row
             cell.ibCount.isHidden = false
             cell.ibCount.text = "\(countArray[indexPath.row])"
             cell.ibStepper.addTarget(self, action: #selector(stepperValueChanged(sender:)), for: .valueChanged)}
    return cell
     }

4.objc functions

 @objc func stepperValueChanged(sender : UIStepper){
     if sender.stepValue != 0{
         countArray[sender.tag] = Int(sender.value)
     }
     ibTableView.reloadData()
 }
 @objc func addPressed(sender : UIButton){

     countArray[sender.tag] = 1//countArray[sender.tag] + 1
     ibTableView.reloadData()
 }