iosswiftxcodeuitableviewuistepper

Updating label in UITableViewCell with UIStepper in Swift


I'm a Swift beginner and I'm trying to make a simple app for ordering food. The user could add a new order by setting food name, price and serving. After adding an order, that order will be shown on the tableView as a FoodTableViewCell, and the user could change the serving with an UIStepper called stepper in each cell. Each order is a FoodItem stored in an array called foodList, and you can see all orders listed in a tableView in ShoppingListVC.

My problem is: When I press "+" or "-" button on stepper, my servingLabel doesn't change to corresponding value. I tried to use NotificationCenter to pass serving value to stepper, and store new value back to food.serving after stepperValueChanged with delegate pattern. However, there still seems to be some bugs. I've been kind of confused after browsing lots of solutions on the Internet. Any help is appreciated.

Update

I removed NotificationCenter and addTarget related methods as @Tarun Tyagi 's suggestion. Now my UIStepper value turns back to 1 whereas the servingLabels are showing different numbers of serving. Since NotificationCenter doesn't help, how can I connect the label and stepper value together? Is it recommended to implement another delegate?

Here are my codes(Updated on July 8):

FoodItem

class FoodItem: Equatable {
    
    static func == (lhs: FoodItem, rhs: FoodItem) -> Bool {
        return lhs === rhs
    }
    
    var name: String
    var price: Int
    var serving: Int
    var foodID: String
    
    init(name: String, price: Int, serving: Int) {
        self.name = name
        self.price = price
        self.serving = serving
        self.foodID = UUID().uuidString
    }
}

ViewController

import UIKit

class ShoppingListVC: UIViewController, UITableViewDataSource, UITableViewDelegate {
    
    var foodList = [FoodItem]()
    
    @IBOutlet weak var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.delegate = self
        tableView.dataSource = self
       
        ...
        
        for i in 1...5 {
            let testItem = FoodItem(name: "Food\(i)", price: Int.random(in: 60...100), serving: Int.random(in: 1...10))
            self.foodList.append(testItem)
        }
    }
    
    // MARK: - Table view data source
    
    ...
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "foodCell", for: indexPath) as! FoodTableViewCell
        
        let food = foodList[indexPath.row]
        cell.nameLabel.text = food.name
        cell.priceLabel.text = "$\(String(food.price)) / serving"
        cell.servingLabel.text = "\(String(food.serving)) serving"
        
        cell.stepper.tag = indexPath.row
        
        cell.delegate = self

        return cell
    }
}

// MARK: - FoodTableViewCellDelegate Method.

extension ShoppingListVC: FoodTableViewCellDelegate {
    
    func stepper(_ stepper: UIStepper, at index: Int, didChangeValueTo newValue: Double) {
        let indexPath = IndexPath(item: index, section: 0)
        guard let cell = tableView.cellForRow(at: indexPath) as? FoodTableViewCell else { return }
        let foodToBeUpdated = foodList[indexPath.row]
        
        print("foodToBeUpdated.serving: \(foodToBeUpdated.serving)")
        
        foodToBeUpdated.serving = Int(newValue)
        print("Value changed in VC: \(newValue)")
        
        cell.servingLabel.text = "\(String(format: "%.0f", newValue)) serving"
    }
}

TableViewCell

import UIKit
    
    protocol FoodTableViewCellDelegate: AnyObject {
      func stepper(_ stepper: UIStepper, at index: Int, didChangeValueTo newValue: Double)
    }
    
    class FoodTableViewCell: UITableViewCell {
        
        @IBOutlet weak var nameLabel: UILabel!
        @IBOutlet weak var priceLabel: UILabel!
        @IBOutlet weak var servingLabel: UILabel!
        @IBOutlet weak var stepper: UIStepper!
        
        weak var delegate: FoodTableViewCellDelegate?
        
        @IBAction func stepperValueChanged(_ sender: UIStepper) {
            sender.minimumValue = 1
            servingLabel.text = "\(String(format: "%.0f", sender.value)) serving"
            // Pass the new value to ShoppingListVC and notify which cell to update using tag.
            print("sender.value: \(sender.value)")
            delegate?.stepper(stepper, at: stepper.tag, didChangeValueTo: sender.value)
        }
        
        override func awakeFromNib() {
            super.awakeFromNib()
            print(stepper.value)
        }
    }

Solution

  • Initially FoodTableViewCell is the ONLY target for UIStepper value changed (looking at @IBAction inside FoodTableViewCell).

    When you dequeue a cell to display on screen, you call -

    cell.stepper.addTarget(self, action: #selector(stepperValueChanged(_:)), for: .valueChanged)
    

    which causes your ShoppingListVC instance to be added as an additional target every time a cellForRow call is executed.

    Things to fix :

    1. Remove all of your NotificationCenter related code from both classes.
    2. Remove cell.stepper.addTarget() line as well.

    This would give you a better idea of why it is happening this way. Update your question with these changes in case you still don't have what you want.


    UPDATE

    // Inside cellForRow
    cell.stepper.value = food.serving