Changing a value with the stepper

I have a stepper that changes a value in my Horse struct, but at the moment I can only increase the value. I need to "refresh" the page to show the new value in the interface, and that starts the process from scratch. Also, if the horse has a value already the stepper changes it to 0 before proceeding.

Code Block
    func addButtons() {
        for (feed,amount) in feedInventory where amount > 0 {
            let filteredProducts = feedProducts.filter { $0.code.contains(feed)}
            for feedProduct in filteredProducts {
                let horizontalStack = UIStackView()
                horizontalStack.axis = .horizontal
                horizontalStack.alignment = .firstBaseline
                let rationAmount = UILabel(frame: .zero)
                let rationIndex = Int(rationArrayPosition[feedProduct.code]!)
                rationAmount.text = "\(myHorses[horseIndex].ration[rationIndex])"
                horizontalStack.addArrangedSubview(rationAmount)
                let stepper = UIStepper(frame: .zero)
                stepper.minimumValue = 0
                stepper.maximumValue = 900
                stepper.wraps = false
                stepper.autorepeat = false
                stepper.addTarget(self, action: #selector(stepperValueChanged(_:)), for: .valueChanged)
                stepper.accessibilityLabel = "\(feedProduct.code)"
                horizontalStack.addArrangedSubview(stepper)
                feedStack.addArrangedSubview(horizontalStack)
                }}}
    func removeButtons() {
        let rows = feedStack.arrangedSubviews
            .filter {$0 is UIStackView}
        for row in rows {
            feedStack.removeArrangedSubview(row)
                row.removeFromSuperview()
            }}
   
    @objc func stepperValueChanged(_ sender:UIStepper!) {
        let rationIndex = rationArrayPosition[sender.accessibilityLabel!]
//This is where I have trouble*
        myHorses[horseIndex].ration[rationIndex ?? 00] += Int(sender.value)
        removeButtons()
        addButtons()
      }
  • I can't use = Int(sender.value) because this resets to 0 each time I press the button, but how can I update the values without using removeButtons() and addButtons()?

Let me know if more code is needed, I think I'm just using the stepper incorrectly though
Answered by OOPer in 662246022

struct FeedProduct {

Thanks. If you created a new project and copied the code in your opening post, you would have found what were missing more easily.


I can't use = Int(sender.value) because this resets to 0 each time I press the button,

I guess you cannot use = Int(sender.value) because you are not setting the value property of the stepper.

how can I update the values without using removeButtons() and addButtons()?

You just need to update the text property of the corresponding UILabel in the action method stepperValueChanged.
To achieve this, you need to access the right UILabel in that method.

To make things easier, you can encapsulate each row into a class:
LabelStepper.swift
Code Block
import UIKit
class LabelStepper: UIStackView {
typealias ActionHandler = (LabelStepper)->Void
/// A handler called on `.valueChanged` of `stepper`
var onValueChanged: ActionHandler?
let label: UILabel
let rationAmount: UILabel //<- The right UILabel
let stepper: UIStepper
init(text: String, value: Int,
onValueChanged: ActionHandler?) {
let label = UILabel(frame: .zero)
let rationAmount = UILabel(frame: .zero)
let stepper = UIStepper(frame: .zero)
self.label = label
self.rationAmount = rationAmount
self.stepper = stepper
self.onValueChanged = onValueChanged
super.init(frame: .zero)
let horizontalStack = self
horizontalStack.axis = .horizontal
horizontalStack.alignment = .firstBaseline //<- You can try other values...
//horizontalStack.distribution = .fillEqually
label.text = text
horizontalStack.addArrangedSubview(label)
rationAmount.text = "\(value)"
horizontalStack.addArrangedSubview(rationAmount)
stepper.value = Double(value) //<- Please do not forget to set `value`
stepper.minimumValue = 0
stepper.maximumValue = 900
stepper.wraps = false
stepper.autorepeat = false
stepper.addTarget(self, action: #selector(stepperValueChanged(_:)), for: .valueChanged)
horizontalStack.addArrangedSubview(stepper)
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func stepperValueChanged(_ sender: UIStepper) {
rationAmount.text = "\(Int(sender.value))" //<- Update the `text` property of the right UILabel
onValueChanged?(self)
}
}

And use it in the VC:
Code Block
func addButtons() {
for (feed,amount) in feedInventory where amount > 0 {
let filteredProducts = feedProducts.filter { $0.code.contains(feed)}
for feedProduct in filteredProducts {
let rationIndex = rationArrayPosition[feedProduct.code]!
let value = myHorses[horseIndex].ration[rationIndex]
let horizontalStack = LabelStepper(
text: "\(feedProduct.name)",
value: value
) {labelStepper in
myHorses[horseIndex].ration[rationIndex] = Int(labelStepper.stepper.value)
}
horizontalStack.stepper.accessibilityLabel = "\(feedProduct.code)"
feedStack.addArrangedSubview(horizontalStack)
}
}
}


Why does your stepper have a zero frame ? And stackView no frame defined ?
I'm not sure to be honest, this code was suggested in another post and I altered it, but there was never a frame defined in the first stackView. The layout works without it for now. Does this affect how the stepper alters values in the model?

Let me know if more code is needed,

Please show everything needed to compile your code.
VC
Code Block
    func addButtons() {
        for (feed,amount) in feedInventory where amount > 0 {
            let filteredProducts = feedProducts.filter { $0.code.contains(feed)}
            for feedProduct in filteredProducts {
                let horizontalStack = UIStackView(frame: .zero)
                horizontalStack.axis = .horizontal
                horizontalStack.alignment = .firstBaseline
                let label = UILabel(frame: .zero)
                label.text = "\(feedProduct.name)"
                horizontalStack.addArrangedSubview(label)
                let rationAmount = UILabel(frame: .zero)
                let rationIndex = Int(rationArrayPosition[feedProduct.code]!)
                rationAmount.text = "\(myHorses[horseIndex].ration[rationIndex])"
                horizontalStack.addArrangedSubview(rationAmount)
                let stepper = UIStepper(frame: .zero)
                stepper.minimumValue = 0
                stepper.maximumValue = 900
                stepper.wraps = false
                stepper.autorepeat = false
                stepper.addTarget(self, action: #selector(stepperValueChanged(_:)), for: .valueChanged)
                stepper.accessibilityLabel = "\(feedProduct.code)"
                horizontalStack.addArrangedSubview(stepper)
                feedStack.addArrangedSubview(horizontalStack)
                }}}
    func removeButtons() {
        let rows = feedStack.arrangedSubviews
            .filter {$0 is UIStackView}
        for row in rows {
            feedStack.removeArrangedSubview(row)
                row.removeFromSuperview()
            }
        }
    @objc func stepperValueChanged(_ sender:UIStepper!) {
        let rationIndex = rationArrayPosition[sender.accessibilityLabel!]
        myHorses[horseIndex].ration[rationIndex ?? 00] += Int(sender.value)
        print(myHorses[horseIndex].ration[rationIndex ?? 00])
        removeButtons()
        addButtons()
      }


Model
Code Block
var feedInventory: [ String : Int ] = [
    "oats" : 0,
    "soyPellets" : 20
]
var feedProducts = [
    FeedProduct(name: "Oats", code: "oats"),
    FeedProduct(name: "Soy Pellets", code: "soyPellets")
]
var rationArrayPosition: [ String : Int ] = [
    "oats" : 0,
    "soyPellets" : 1
]
struct Horse {
    var name : String!
var ration : [Int]
}
var horseIndex = 0
var myHorses = [
    Horse(name: "Donnerhall", ration: [2,0]),
    Horse(name: "Celeste", ration: [3,1])
]




Thanks for adding code. But with all your shown code, Xcode generates some errors:
Code Block
Cannot find 'FeedProduct' in scope
Cannot find 'FeedProduct' in scope
Cannot find 'feedStack' in scope

Have your created a brand-new project to check what are needed?
Forgot that one:

   @IBOutlet weak var feedStack: UIStackView!

    and feedProduct is defined here in the for loop:
Code Block
for feedProduct in filteredProducts {

    and feedProduct is defined here in the for loop:

It's feedProduct, not FeedProduct.
struct FeedProduct {

    var name : String
    var code : String
}

this code was suggested in another post … Does this affect how the stepper alters values in the model?

That should not affect directly, but could be the stepper does not receive the message ?
With the limited code we had, we could just guess.
We probably have all the code now, but in a lot of small pieces. Do you expect us to reassemble (I will not) or are you going to publish something more directly usable ?

In any case, it is always risky to paste code without really understanding what it is doing.


It's tricky on this forum as I can't edit a post or comments, but I'll post it again with edits:

VC

Code Block
    func addButtons() {
        for (feed,amount) in feedInventory where amount > 0 {
            let filteredFeedProducts = feedProducts.filter { $0.code.contains(feed)}
            for filteredFeed in filteredFeedProducts {
                let horizontalStack = UIStackView(frame: .zero)
                horizontalStack.axis = .horizontal
                horizontalStack.alignment = .firstBaseline
            
                let label = UILabel(frame: .zero)
                label.text = "\(filteredFeed.name)"
                horizontalStack.addArrangedSubview(label)
              
                let rationAmount = UILabel(frame: .zero)
                let rationIndex = Int(rationArrayPosition[filteredFeed.code]!)
                rationAmount.text = "\(myHorses[horseIndex].ration[rationIndex])"
                horizontalStack.addArrangedSubview(rationAmount)
                
                let stepper = UIStepper(frame: .zero)
                stepper.minimumValue = 0
                stepper.maximumValue = 900
                stepper.wraps = false
                stepper.autorepeat = false
                stepper.addTarget(self, action: #selector(stepperValueChanged(_:)), for: .valueChanged)
                stepper.accessibilityLabel = "\(filteredFeed.code)"
                horizontalStack.addArrangedSubview(stepper) 
                feedStack.addArrangedSubview(horizontalStack)
                }}}
    func removeButtons() {
        let rows = feedStack.arrangedSubviews
            .filter {$0 is UIStackView}
        for row in rows {
            feedStack.removeArrangedSubview(row)
                row.removeFromSuperview()
            }
        }
    
    @objc func stepperValueChanged(_ sender:UIStepper!) {
        let rationIndex = rationArrayPosition[sender.accessibilityLabel!]
        myHorses[horseIndex].ration[rationIndex ?? 00] += Int(sender.value)
        print(myHorses[horseIndex].ration[rationIndex ?? 00])
      removeButtons()
        addButtons()
      }

Model
Code Block
var feedInventory: [ String : Int ] = [   
"oats" : 0,   
"soyPellets" : 20]
var feedProducts = [   
FeedProduct(name: "Oats", code: "oats"),   
FeedProduct(name: "Soy Pellets", code: "soyPellets")]
var rationArrayPosition: [ String : Int ] = [   
"oats" : 0,   
"soyPellets" : 1]
struct Horse {   
var name : String!
var ration : [Int]
}
var horseIndex = 0
var myHorses = [   
Horse(name: "Donnerhall", ration: [2,0]),   
Horse(name: "Celeste", ration: [3,1])]
@IBOutlet weak var feedStack: UIStackView!
struct FeedProduct {
  var name : String
  var code : String
}



Accepted Answer

struct FeedProduct {

Thanks. If you created a new project and copied the code in your opening post, you would have found what were missing more easily.


I can't use = Int(sender.value) because this resets to 0 each time I press the button,

I guess you cannot use = Int(sender.value) because you are not setting the value property of the stepper.

how can I update the values without using removeButtons() and addButtons()?

You just need to update the text property of the corresponding UILabel in the action method stepperValueChanged.
To achieve this, you need to access the right UILabel in that method.

To make things easier, you can encapsulate each row into a class:
LabelStepper.swift
Code Block
import UIKit
class LabelStepper: UIStackView {
typealias ActionHandler = (LabelStepper)->Void
/// A handler called on `.valueChanged` of `stepper`
var onValueChanged: ActionHandler?
let label: UILabel
let rationAmount: UILabel //<- The right UILabel
let stepper: UIStepper
init(text: String, value: Int,
onValueChanged: ActionHandler?) {
let label = UILabel(frame: .zero)
let rationAmount = UILabel(frame: .zero)
let stepper = UIStepper(frame: .zero)
self.label = label
self.rationAmount = rationAmount
self.stepper = stepper
self.onValueChanged = onValueChanged
super.init(frame: .zero)
let horizontalStack = self
horizontalStack.axis = .horizontal
horizontalStack.alignment = .firstBaseline //<- You can try other values...
//horizontalStack.distribution = .fillEqually
label.text = text
horizontalStack.addArrangedSubview(label)
rationAmount.text = "\(value)"
horizontalStack.addArrangedSubview(rationAmount)
stepper.value = Double(value) //<- Please do not forget to set `value`
stepper.minimumValue = 0
stepper.maximumValue = 900
stepper.wraps = false
stepper.autorepeat = false
stepper.addTarget(self, action: #selector(stepperValueChanged(_:)), for: .valueChanged)
horizontalStack.addArrangedSubview(stepper)
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func stepperValueChanged(_ sender: UIStepper) {
rationAmount.text = "\(Int(sender.value))" //<- Update the `text` property of the right UILabel
onValueChanged?(self)
}
}

And use it in the VC:
Code Block
func addButtons() {
for (feed,amount) in feedInventory where amount > 0 {
let filteredProducts = feedProducts.filter { $0.code.contains(feed)}
for feedProduct in filteredProducts {
let rationIndex = rationArrayPosition[feedProduct.code]!
let value = myHorses[horseIndex].ration[rationIndex]
let horizontalStack = LabelStepper(
text: "\(feedProduct.name)",
value: value
) {labelStepper in
myHorses[horseIndex].ration[rationIndex] = Int(labelStepper.stepper.value)
}
horizontalStack.stepper.accessibilityLabel = "\(feedProduct.code)"
feedStack.addArrangedSubview(horizontalStack)
}
}
}



I guess you cannot use = Int(sender.value) because you are not setting the value property of the stepper.

I tried setting a value, but 1 is default so this seemed unnecessary. either way, when pressing the button it resets the whole thing to 0 so I only ever get to 1. If I use += Int(sender.value) it works, but will only ever increase. I need to be able to decrease as well


@objc func stepperValueChanged(_ sender: UIStepper) { rationAmount.text = "\(Int(sender.value))" //<- Update the text property of the right UILabel onValueChanged?(self) }

My aim with the VC is to change the value of the ration array in the model, the VC only shows the number listed in the model



but 1 is default so this seemed unnecessary.

You may be mistaking something. If you want to use stepper correctly, you need to set value.

My aim with the VC is to change the value of the ration array in the model, the VC only shows the number listed in the model

Can't you see what is given to onValueChanged?

Please try before saying something based on wrong assumptions.
Aha, I understand now. Sorry for jumping to conclusions. I didn't know it could be done that way :). This might help me move some code from the Vc to the model in other parts of my app :)
Changing a value with the stepper
 
 
Q