Calling a property of a struct with a placeholder?

I have a few places in my app where I need to alter a property of a struct, but it takes a lot of code.

The struct, arrays and variables:
Code Block
struct Horse {
var name: String
    var basicTraining : Float
    var rhythm : Float
    var suppleness : Float
var myHorses = [
Horse(name: "Donnerhall", basicTraining : 0.5, rhythm : 0.2, suppleness : 0.1),
Horse(name: "Bjork", basicTraining : 0.4, rhythm : 0.3, suppleness : 0.1)
]
var horseIndex = 0
var scaleCode = [ basicSkills, rhythmSkills, supplenessSkills ]
var currentScaleCode = basicSkills
var skillIndex = 0


The current code (takes a lot of lines):
Code Block
func trainHorse() {
if myHorses[horseIndex].basicTraining > 100 {
                myHorses[horseIndex].basicTraining = 100 }
if myHorses[horseIndex].basicTraining < 100 {
                myHorses[horseIndex].basicTraining += currentScaleCode[skillIndex].basicBoost }
            
            if myHorses[horseIndex].rhythm > 100 {
                myHorses[horseIndex].rhythm = 100 }
            if myHorses[horseIndex].rhythm < 100 {
                myHorses[horseIndex].rhythm += currentScaleCode[skillIndex].rhythmBoost }
            if myHorses[horseIndex].suppleness > 100 {
                myHorses[horseIndex].suppleness = 100 }
            if myHorses[horseIndex].suppleness < 100 {
                myHorses[horseIndex].suppleness += currentScaleCode[skillIndex].supplenessBoost }
}


What I would like is to simplify this code by doing something like this:

Code Block
var currentTraining = basicTraining //or rhythm or suppleness
currentBoost = basicBoost
func trainHorse() {
if myHorses[horseIndex].currentTraining > 100 {
                myHorses[horseIndex].currentTraining = 100 }
if myHorses[horseIndex].currentTraining < 100 {
                myHorses[horseIndex].currentTraining += currentScaleCode[skillIndex].currentBoost }


But when I call a property using a placeholder, I get the error "Value of type 'Horse' has no member..." I have tried using different types of brackets and parentheses without luck
Answered by OOPer in 655049022
The word placeholder does not seem to be a good one to represent your issue.
Many languages have some feature to access properties indirectly, in Swift, it is called as keyPath.

But Swift is statically typed language and its usage is a little bit difficult:
Code Block
import Foundation
struct Horse {
var name: String
var basicTraining : Float
var rhythm : Float
var suppleness : Float
}
var myHorses = [
Horse(name: "Donnerhall", basicTraining : 0.5, rhythm : 0.2, suppleness : 0.1),
Horse(name: "Bjork", basicTraining : 0.4, rhythm : 0.3, suppleness : 0.1)
]
var horseIndex = 0
var currentTraining: WritableKeyPath<Horse, Float> = \.basicTraining //or \.rhythm or \.suppleness
func trainHorse() {
if myHorses[horseIndex][keyPath: currentTraining] > 100 {
myHorses[horseIndex][keyPath: currentTraining] = 100
}
if myHorses[horseIndex][keyPath: currentTraining] < 100 {
myHorses[horseIndex][keyPath: currentTraining] += currentScaleCode[skillIndex].basicBoost
}
//...
}


You are not showing relevant definitions in The struct, arrays and variables.
So, I do not see how to write currentBoost using keyPath.
There is a } missing line 6 for instance.
How did you define basicSkills and other skills ?

Could you provide complete and exact code ?

But when I call a property using a placeholder

What do you mean by placeHolder ?
I do not see the difference you try to get.

You could start to simplify a little:
Code Block
func trainHorse() {
if myHorses[horseIndex].basicTraining >= 100 {
myHorses[horseIndex].basicTraining = 100
} else {
myHorses[horseIndex].basicTraining += currentScaleCode[skillIndex].basicBoost
}

Accepted Answer
The word placeholder does not seem to be a good one to represent your issue.
Many languages have some feature to access properties indirectly, in Swift, it is called as keyPath.

But Swift is statically typed language and its usage is a little bit difficult:
Code Block
import Foundation
struct Horse {
var name: String
var basicTraining : Float
var rhythm : Float
var suppleness : Float
}
var myHorses = [
Horse(name: "Donnerhall", basicTraining : 0.5, rhythm : 0.2, suppleness : 0.1),
Horse(name: "Bjork", basicTraining : 0.4, rhythm : 0.3, suppleness : 0.1)
]
var horseIndex = 0
var currentTraining: WritableKeyPath<Horse, Float> = \.basicTraining //or \.rhythm or \.suppleness
func trainHorse() {
if myHorses[horseIndex][keyPath: currentTraining] > 100 {
myHorses[horseIndex][keyPath: currentTraining] = 100
}
if myHorses[horseIndex][keyPath: currentTraining] < 100 {
myHorses[horseIndex][keyPath: currentTraining] += currentScaleCode[skillIndex].basicBoost
}
//...
}


You are not showing relevant definitions in The struct, arrays and variables.
So, I do not see how to write currentBoost using keyPath.
Claude, I used your "else" suggestion and cleaned it up a bit
Ooper, I used the same code and I think I can make it work for currentBoost. I left out the skill struct by accident, but it works when I typed it out like this

Code Block
var currentTraining: WritableKeyPath<Horse, Float> = \.basicTraining //or \.rhythm or \.suppleness
var currentBoost: WritableKeyPath<Skill, Float> = \.basicBoost

This might clean up a lot of unnecessary code, but how can I change the \.basicTraining to \.rhythm in the variable? I tried this with no luck

Code Block
currentTraining = \.rhythm
print(currentTraining)


The console reads "Swift.WritableKeyPath<Tobiano.Horse, Swift.Float>"


The console reads "Swift.WritableKeyPath<Tobiano.Horse, Swift.Float>"

Why do you want to print the key path ?

It is not intended for this, just use it to access a property and print the property itself.
The app isn't working as intended, I printed to try to troubleshoot it. Can you show an example of how you would change the currentTraining variable? This is how I have tried to do it:

Code Block
var currentTraining: WritableKeyPath<Horse, Float> = \.basicTraining
var scaleIndex = 0


IBAction with switch statement (scaleIndex is the same as sender.tag, so the user selects the scale (basic, rhythm, suppleness))
Code Block
        switch scaleIndex {
        case 0:
            currentTraining = \.basicTraining
        case 1:
            currentTraining = \.rhythm
        case 2:
            currentTraining = \.suppleness


but this doesn't work

IBAction with switch statement (scaleIndex is the same as sender.tag, so the user selects the scale (basic, rhythm, suppleness))

Code Block
switch scaleIndex {
case 0:
currentTraining = \.basicTraining
case 1:
currentTraining = \.rhythm
case 2:
currentTraining = \.suppleness


but this doesn't work

What do you mean "doesn't work" ? What do you get ? A compiler error ? A crash ?

Could you show the complete code, with definition of all types, so that we can test ?

this doesn't work

Seems you prefer hiding many important parts. I recommend you to show more codes than you think enough.

Please explain what you mean by this doesn't work.

If it causes some error, please show the whole error message


If it does compile and raise no runtime error, please explain these things:

- Show all the operation you have done to your app
  • What is the expected result?

  • What you actually get?

When you show some code, please, at lease, balance the parentheses.
When you show the definition of variables, please clarify where they are defined,
When you say some result of processing is not what you expect, please show when the processing (in this case trainHorse()) is called.

I believe I can help solving the issue if you had shown enough info.
There isn't space here to show the entire code, so I'm trying to collect the bits that count from different parts of the app. The VC I am working with shows a progress bar for each scale on each horse. When I use a lengthy If statement, it works the way it should. However, it requires a lot of extra coding. I wonder if key paths could be a way of shortening my code (in reality there are 7 possibilities instead of three). I think I can make key paths work if I can figure out how to change var currentTraining and var currentBoost. Then I can make an IBAction to change the var and thus make the progress bar show the training progress. For now it only shows basicTraining, because that is what I have manually written in the var currentTraining. I need to do this programmatically with the scalesButton. I think I managed to include everything now, please let me know if there is anything else missing

Code Block
struct Horse {
var name: String
var basicTraining : Float
var rhythm : Float
var suppleness : Float}
var myHorses = [
Horse(name: "Donnerhall", basicTraining : 0.5, rhythm : 0.2, suppleness : 0.1),
Horse(name: "Bjork", basicTraining : 0.4, rhythm : 0.3, suppleness : 0.1)]
var horseIndex = 0
struct Skill {
var basicBoost: Float
var rhythmBoost: Float
var supplenessBoost: Float}
var scaleCode = [ basicSkills, rhythmSkills, supplenessSkills]
var scaleIndex = 0
var basicSkills = [Skill(basicBoost: 0.1, rhythmBoost: 0, supplenessBoost: 0)]
var rhythmSkills = [Skill(basicBoost: 0, rhythmBoost: 0.1, supplenessBoost: 0)]
var supplenessSkills = [Skill(basicBoost: 0, rhythmBoost: 0.1, supplenessBoost: 0.1)]
var skillIndex = 0
var currentTraining: WritableKeyPath<Horse, Float> = \.basicTraining //or \.rhythm or \.suppleness
var currentBoost: WritableKeyPath<Skill, Float> = \.basicBoost
    @IBAction func scalesButton(_ sender: UIButton) {
        scaleIndex = sender.tag
        switch scaleIndex {
        case 0:
            currentTraining = \.basicTraining
        case 1:
            currentTraining = \.rhythm
        case 2:
            currentTraining = \.suppleness
}
//should choose .basicTraining, .rhythm, or .suppleness depending on which scalesButton the user selects. However, some skills can boost multiple stats, so I might have to do a separate if statement for each stat
 
@objc func trainHorse(sender: UIButton){
      if myHorses[horseIndex][keyPath: currentTraining] >= 100 {
        myHorses[horseIndex][keyPath: currentTraining] = 100
        }  else  {
        myHorses[horseIndex][keyPath: currentTraining] += currentScaleCode[skillIndex].currentBoost
            }
//previous code without key paths:
//            if myHorses[horseIndex].basicTraining >= 100 {
//                    myHorses[horseIndex].basicTraining = 100
//            } else  {
//                    myHorses[horseIndex].basicTraining += currentScaleCode[skillIndex].basicBoost
//            }
//
//            if myHorses[horseIndex].rhythm >= 100 {
//                    myHorses[horseIndex].rhythm = 100
//            } else  {
//                    myHorses[horseIndex].rhythm += currentScaleCode[skillIndex].rhythmBoost
//            }
//
//            if myHorses[horseIndex].suppleness >= 100 {
//                    myHorses[horseIndex].suppleness = 100
//            } else  {
//                    myHorses[horseIndex].suppleness += currentScaleCode[skillIndex].supplenessBoost
//            }
        currentScaleCode = scaleCode[scaleIndex]
//the following shows a progress bar with the stat selected with the scalesButton
            skillProgress.progress = myHorses[horseIndex][keyPath: currentTraining]
            skillOutlet.text = "\(currentScaleName): \(myHorses[horseIndex][keyPath: currentTraining])"
        }
previous code:
//switch scaleIndex {
//        case 0:
//            skillProgress.progress = myHorses[horseIndex].basicTraining
//            skillOutlet.text = "\(currentScaleName): \(myHorses[horseIndex].basicTraining)"
//        case 1:
//            skillProgress.progress = myHorses[horseIndex].rhythm
//            skillOutlet.text = "\(currentScaleName): \(myHorses[horseIndex].rhythm)"
//        case 2:
//            skillProgress.progress = myHorses[horseIndex].suppleness
//            skillOutlet.text = "\(currentScaleName): \(myHorses[horseIndex].suppleness)"


It seems skillIndex never changes, is always 0.
Is that the case ?

Once you have changed currentTraining, where do you expect something to change ?
Where do you notice it didn't change ?

trainHorse() has a UIButton parameter.
But it is not declared as an IBAction.
Where do you call it ?

So we still miss a lot of information to find where are the flaws in your code.



skillIndex refers to different elements of the skills arrays, in this case it's not important. Another set of buttons selects a skill from each array.
the skill buttons are programmatically added when the user selects a scale.

Code Block       for skillIndex in currentScaleCode.indices {
                    let button = UIButton()
                    button.tag = skillIndex
                    button.setTitleColor(.white, for: UIControl.State.normal)
                    button.addTarget(self, action: #selector(self.trainHorse(sender:)), for: .touchUpInside)
                    button.setTitle(currentScaleCode[skillIndex].name, for: UIControl.State.normal)
                    button.backgroundColor =  colorLiteral(red: 0.005273803137, green: 0.4785152674, blue: 0.3960535526, alpha: 1)
                    buttonsStack.addArrangedSubview(button)
                }
         buttonsStack.translatesAutoresizingMaskIntoConstraints = false
        


These buttons trigger the trainHorse function, which updates a progress bar showing progress within the selected scale. For now it only shows .basicSkills because I have been unable to change this to anything else. If I could change the currentTraining key path to something different, I could update the progress bar programmatically with the trainHorse function. All I need to know is how to change the var currentTraining

I think I managed to include everything now,

Please remember, do not leave parentheses unbalanced.

please let me know if there is anything else missing

Got it.

The VC I am working with shows a progress bar for each scale on each horse. 

Can you show the name of the VC? And can you tell us what is scale?

I guess skillProgress is an @IBOutlet of type UIProgressView! to show the progress, OK?

I need to do this programmatically with the scalesButton.

I guess the VC has 3 scalesButtons and the action of all the buttons connected to scalesButton(_:), right?

Still, 3 identifiers are missing, currentScaleCode, skillOutlet and currentScaleName.
I guess these as:
Code Block
@IBOutlet weak var skillOutlet: UILabel!
var currentScaleCode: [Skill] = basicSkills
var currentScaleName: String = ""


Guess how many things I have guessed till now. If you had shown enough code, I could have concentrate on how to fix your issue.

I guess your the VC looks like something like this:
Code Block
class ViewController: UIViewController {
//...
@IBOutlet weak var skillProgress: UIProgressView!
@IBOutlet weak var skillOutlet: UILabel!
var currentScaleCode: [Skill] = basicSkills
var currentScaleName: String = ""
var myHorses = [
Horse(name: "Donnerhall", basicTraining : 0.5, rhythm : 0.2, suppleness : 0.1),
Horse(name: "Bjork", basicTraining : 0.4, rhythm : 0.3, suppleness : 0.1),
]
var horseIndex = 0
var scaleCode = [ basicSkills, rhythmSkills, supplenessSkills]
var scaleIndex = 0
var skillIndex = 0
var currentTraining: WritableKeyPath<Horse, Float> = \.basicTraining //or \.rhythm or \.suppleness
var currentBoost: WritableKeyPath<Skill, Float> = \.basicBoost
@IBAction func scalesButton(_ sender: UIButton) {
scaleIndex = sender.tag
switch scaleIndex {
case 0:
currentTraining = \.basicTraining
case 1:
currentTraining = \.rhythm
case 2:
currentTraining = \.suppleness
//other cases...
default:
break
}
}
@objc func trainHorse(sender: UIButton) {
//...
}
}

(I guess you may be making some of the vars global, and it is not considered to be a good design, but that is not important here.)


Seeing the first part of your previous code, you are modifying 3 (or more in your actual code, but that's another thing) properties in the same way, regardless of the value of scaleIndex.
You may need to repeat it 3 times.

In the latter previous code, you do only 1 thing depending on scaleIndex. As the currentTraining is updated according to scaleIndex, the code should be working the same as the previous code.

Code Block
@objc func trainHorse(sender: UIButton) {
// The first part...
for (training, boost) in [
(\Horse.basicTraining, \Skill.basicBoost),
(\Horse.rhythm, \Skill.rhythmBoost),
(\Horse.suppleness, \Skill.supplenessBoost),
//...
] {
if myHorses[horseIndex][keyPath: training] >= 100 {
myHorses[horseIndex][keyPath: training] = 100
} else {
myHorses[horseIndex][keyPath: training] += currentScaleCode[skillIndex][keyPath: boost]
}
}
currentScaleCode = scaleCode[scaleIndex]
//the following shows a progress bar with the stat selected with the scalesButton
//The latter part...
skillProgress.progress = myHorses[horseIndex][keyPath: currentTraining]
skillOutlet.text = "\(currentScaleName): \(myHorses[horseIndex][keyPath: currentTraining])"
}


That's all I can say under the current info.


When we can access the original project, it is very kind and useful your showing picked up parts of your code. But please remember we readers cannot access your project other than the shown parts.

You may want to use Link feature of this site, which enables us to link longer files.
@imey

All I need to know is how to change the var currentTraining

The way you do it in @IBAction func scalesButton switch seems correct.
So the problem is elsewhere.

Could be:
  • the @IBAction func scalesButton is not called.

=> check by adding a print at the beginning of the func.
  • once you have updated, you don't reset the relevant fields with the new value

=> where do you use the new value of currentTraining once updated ? We don't see it in the published code
I don't think it can work in the trainHorse function after all, but there is still hope for the progress bar. This is updated every time the trainHorse button is pressed.

Code Block    
skillProgress.progress = myHorses[horseIndex][keyPath: currentTraining]           
skillOutlet.text = "\(currentScaleName): \(myHorses[horseIndex][keyPath: currentTraining])"        }

The scalesButton updates the currentTraining var, then the trainHorse button updates skillProgress

I don't think it can work in the trainHorse function after all, but there is still hope for the progress bar. This is updated every time the trainHorse button is pressed.

Code Block
skillProgress.progress = myHorses[horseIndex][keyPath: currentTraining]
skillOutlet.text = "\(currentScaleName): \(myHorses[horseIndex][keyPath: currentTraining])"
  }

The scalesButton updates the currentTraining var, then the trainHorse button updates skillProgress

So, where is this code ? In which function now ?
Where is this func called.

It is nearly impossible to understand what you do if you keep providing such partial information.


Calling a property of a struct with a placeholder?
 
 
Q