Reputation: 91
I am fairly new (a few months in) to swift so please excuse me if the question is trivial or duplicated.
I am trying to build a class which includes properties as well as an array of tuples (with 3 parameters) within which I store the keypath to the properties in the class as well as some other parameters which I would like to use to initialise/modify the class's properties
The code below is an example of what I am trying to achieve. The real code is a lot more complex and uses about a hundred properties within the class.
class Strategy
{
var name = ""
var value = 0.0
var position = 0
var param = [(path: PartialKeyPath<Strategy>, label: String, value: Any)]()
init()
{
self.param.append((path: \Strategy.name, label: "Name",value: "Generic String"))
self.param.append((path: \Strategy.value, label: "Value",value: 375.8))
self.param.append((path: \Strategy.position, label: "Position",value: 34))
for paramIndex in 0..<self.param.count {
let kp = self.param[paramIndex].path
self[keyPath: kp] = self.param[paramIndex].value
}
}
var myStrat = Strategy()
let kp = myStrat.param[0].path
print(myStrat[keyPath: kp])
myStrat[keyPath: kp] = " New String"
The reason for this approach is that I then have to display all the properties in an NSTableView so I can load the table via a loop on param rather than each property at the time. Also, as the properties need to be editable via the NSTableView, I can use the row index, find the keypath to the property to modify and modify it.
I am getting a "Cannot assign to immutable expression of type 'Any'" error on:
self[keyPath: kp] = self.param[paramIndex].value
as well as on
myStrat[keyPath: kp] = " New String"
which exemplifies how I would like to change the property name in the Strategy class
I think it has to do with self not being mutable but I cannot figure out how to solve this.
Thank you and apologies if this is a silly question of an inexperienced programmer.
Upvotes: 3
Views: 2458
Reputation: 91
Thank you. Whilst playing with ReferenceWritableKeyPath I found a way that allows me not to specify the Value by putting it as Any. I then declare my properties in the class as Any and it seems to work:
class Strategy
{
var name = "" as Any
var value = 0.0 as Any
var position = 0 as Any
var param = [(path: ReferenceWritableKeyPath<Strategy, Any>, label: String, value: Any)]()
init()
{
self.param.append((path: \Strategy.name, label: "Name",value: "Generic String"))
self.param.append((path: \Strategy.value, label: "Value",value: 375.8))
self.param.append((path: \Strategy.position, label: "Position",value: 34))
for paramIndex in 0..<self.param.count {
let kp = self.param[paramIndex].path
self[keyPath: kp] = self.param[paramIndex].value
}
}
}
var myStrat = Strategy()
let kp = myStrat.param[0].path
print(myStrat[keyPath: kp])
myStrat[keyPath: kp] = "New String"
print(myStrat[keyPath: kp])
My only remaining doubt, being a novice to Swift if this may create problems in the future. I have checked the types I get by using the keypaths and they match the properties types (String, Double and Int) even as I declared them as Any but initialised them as different types
Upvotes: 0
Reputation: 47896
You need to use ReferenceWritableKeyPath<Root,Value>
to modify properties of reference type objects (class
instances). One sad thing is that you need to specify both type Root
and Value
statically.
For example:
if let keyPath = kp as? ReferenceWritableKeyPath<Strategy, String> {
myStrat[keyPath: keyPath] = " A String"
}
So, to make your code work, you may need something like this:
protocol AnyKeyAccessible: class {
subscript (anyKeyPath keyPath: PartialKeyPath<Strategy>) -> Any? {get set}
}
extension AnyKeyAccessible {
subscript (anyKeyPath keyPath: PartialKeyPath<Strategy>) -> Any? {
get {
switch keyPath {
case let keyPath as KeyPath<Self, String>:
return self[keyPath: keyPath]
case let keyPath as KeyPath<Self, Double>:
return self[keyPath: keyPath]
case let keyPath as KeyPath<Self, Int>:
return self[keyPath: keyPath]
// More cases may be needed...
default:
return nil
}
}
set {
switch keyPath {
case let keyPath as ReferenceWritableKeyPath<Self, String>:
self[keyPath: keyPath] = newValue as! String
case let keyPath as ReferenceWritableKeyPath<Self, Double>:
self[keyPath: keyPath] = newValue as! Double
case let keyPath as ReferenceWritableKeyPath<Self, Int>:
self[keyPath: keyPath] = newValue as! Int
// More cases may be needed...
default:
break
}
}
}
}
And then you can re-write your code using it:
class Strategy: AnyKeyAccessible, CustomStringConvertible {
var name = ""
var value = 0.0
var position = 0
var param = [(path: PartialKeyPath<Strategy>, label: String, value: Any)]()
init() {
self.param.append((path: \Strategy.name, label: "Name",value: "Generic String"))
self.param.append((path: \Strategy.value, label: "Value",value: 375.8))
self.param.append((path: \Strategy.position, label: "Position",value: 34))
for paramIndex in 0..<self.param.count {
let kp = self.param[paramIndex].path
self[anyKeyPath: kp] = self.param[paramIndex].value
}
}
//For debugging
var description: String {
return "name=\(name), value=\(value), position=\(position)"
}
}
var myStrat = Strategy()
let kp = myStrat.param[0].path
print(myStrat[anyKeyPath: kp]) //-> Optional("Generic String")
myStrat[anyKeyPath: kp] = " New String"
print(myStrat) //-> name= New String, value=375.8, position=34
But, as I wrote in the comment of the code above, you may need many more case
s to work with actual target classes. I'm not sure this really solves your issue.
Upvotes: 6