Search code examples
swiftuitableviewuistepper

reloadData() called when scrolling is affecting internal data


So it appears that a Table View in Swift dynamically reloads data onto the screen based on the scroll position. However, in my case, I don't want the mechanism to work this way since the items in my table view update the total price of the user's order and is unnecessarily screwing up the total price value. When I scroll up or down, the cells in which the reload data is being called on are reperforming the math and is leading to the incorrect total price.

My wish is to recalculate the price using the reloadData() function only when the UIStepper is pressed on a particular cell rather than on scrolling also. I've done this by having an IBAction function that calls reloadData when the stepper is pressed. The problem with the math and internal data occurs when the table view is scrolled on and is repeatedly calling the reloadData function as the visible cells are changed which I don't want it to do for any form of scrolling whatsoever.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let add_on = JSON(self.add_ons[indexPath.row])
        let cell = tableView.dequeueReusableCell(withIdentifier: "AddOnCell", for: indexPath) as! AddOnTableViewCell

        quantities[indexPath.row] = cell.quantity
        //cell.quantityLabel.text = String(quantities[indexPath.row])

        let price = Double(add_on["price"].int!)/100.0
        let quantity:Double = Double(quantityLabel.text!)!

        //when user tries to increase add-on quantity beyond ticket quantity
        if(Int(quantity) < cell.quantity){
            print("QUANTITY UPDATED")
            cell.quantity -= 1
            quantities[indexPath.row] = cell.quantity
        }
        self.addOnPrice = updateTotal(quantities: self.quantities)
        totalPriceLabel.text = (Double(event_subtotal * quantity) + addOnPrice).dollarRound()

        cell.addonLabel?.text = add_on["name"].string! + " (+$\(price.roundTo(places: 2))" + ")"

        return cell
}

func updateTotal(quantities: [Int]) -> Double{
    var result:Double = 0.0
    for i in 0..<quantities.count{
        let curr_addon = JSON(self.add_ons[i])
        let price = Double(curr_addon["price"].int!)/100.0
        result += Double(quantities[i]) * price
    }
    return result.dollarRoundDouble()
}

@IBAction func stepperClicked(_ sender: UIStepper) {
    self.addOnPrice = 0.0
    AddOnTableView.reloadData()
}

Below is the cell data model:

class AddOnTableViewCell: UITableViewCell{

    @IBOutlet weak var addonLabel: UILabel!
    @IBOutlet weak var quantityLabel: UILabel!
    @IBOutlet weak var quantityStepper: UIStepper!
    var quantity : Int  = 0 {
        didSet{
            self.quantityLabel.text = String(quantity)
            self.quantityStepper.value = Double(quantity)
        }
    }

    //ref: https://stackoverflow.com/questions/42876739/swift-increment-label-with-stepper-in-tableview-cell
    @IBAction func quantityStep(_ sender: UIStepper) {
        self.quantity = Int(sender.value)
        self.quantityLabel.text = String(quantity)
    } 
}

Behavior: Upon scrolling, the total price calculated is changing without stepper interaction. Based on scroll position, the quantity for 1 item is "exchanging" the quantity value with another item. First item quantity becoming 1 updates last item's quantity for some reason. Unable to debug this behavior.

UI for reference enter image description here


Solution

  • In UIKit, cells are being reused as you scroll the list. Assume that the cell values just come and go. It should not hold long term values. Calculating of the total should not be in cellForRow method. First of all, you are missing a data model to hold all the data. The way of doing it, is that you create a structure that represents an add-on (has quantity, price, etc). When the json is received, you parse all of it and store it in the array. The array is then used as you master source. The cellForRow should use the data there, and pass the structure to the AddOnTableViewCell so that everything is separated nicely. When the user hits the quantity step, it should call a delegate class that you create, which is implemented in the view controller, which updates the array with the updated quantity and then does the total. Assume that the cell values just come and go.

    So create a new class:

    protocol AddOnUpdated: class {
        func quantityUpdated(label: String, value: Int)
    }
    

    Then add a member to your cell {

    class AddOnTableViewCell: UITableViewCell{
      weak var delegate: AddOnUpdated?
    
      func quantityStep {
         .. same code
         delegste?.quantityUpdated(label, value)
      }
    }
    

    Then in the view controller, do not update the totals when you create the cell. Just implement the delegate, and set the delegate value when you create the cell to self.

    So now, the delegate should be called whenever the value of any cell is updated. At that point, you need to update your values array, and update the total.

    Hope all this makes sense.