Marco
Marco

Reputation: 91

Trying to Modify Class Properties with PartialKeyPath in Swift

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

Answers (2)

Marco
Marco

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

OOPer
OOPer

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 cases to work with actual target classes. I'm not sure this really solves your issue.

Upvotes: 6

Related Questions