Swift 5.7, can you use a string to dynamically use a keyPath with Structs?

157 Views Asked by At
struct Server: Codable {
 let cats: Int
 let dogs: Int
 let fishies: Int
}

It must be Codable.

let x = Server(cats: 42, dogs: 13, fishes: 777)

We can use keypaths:

print( x[keyPath: \.dogs] )

Which would print "13".

Is there a way to use a string for the keypath? So, something like

let str = ".dogs"
print( x[ keyPath: \$str ] ]

or perhaps

let s = ".dogs"
let k = KeyPath(fromString: s)
print( x[keyPath: k] )

(Note, I appreciate there are any number of other approaches, eg, use sql, dictionaries, switch statement, etc etc. The question at hand is as stated, TY)

2

There are 2 best solutions below

6
Fattie On BEST ANSWER

Swift 5.7, can you use a string to dynamically use a keyPath with Structs?

For the record, the answer is No, you cannot, as of Swift5.


If you need to access struct fields, by string (for example in a typical "column definition" setup), just IMO the best thing to do is simply something like ...

func structField(viaString key: String) -> String {
    switch key {
    case "height": return String(self.height)
    case "lat": return self.lat
    case "long": return self.long
    ... etc
    ... etc

which would vary in your use case.

4
lorem ipsum On

With struct you have KeyPath and WritableKeyPath. As written your Server is only compatible with KeyPath because all your properties are let.

print(x[keyPath: \.dogs])

If you change your properties to make them mutable var you can use.

struct Server: Codable {
    var cats: Int
    var dogs: Int
    var fishies: Int
}

var x = Server(cats: 42, dogs: 13, fishies: 777)

print(x[keyPath: \.dogs])

x[keyPath: \.fishies] = 888

print(x)

If you insist on using String you can use Codable and JSONSerialization but it is a bad idea and counter to type safe principle because any of it could fail at any point with no warning.

extension Server {
    subscript(_ keyPath: String) -> Any? {
        get {
            if let data = try? JSONEncoder().encode(self)
                , var dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { //Any of this could fail silently at any time
                return dict[keyPath]
            } else {
                return nil
            }
        }
        set {
            if let data = try? JSONEncoder().encode(self)
                , var dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { //Any of this could fail silently at any time
                dict[keyPath] = newValue
                
                if let newData = try? JSONSerialization.data(withJSONObject: dict), let newObj = try? JSONDecoder().decode(Self.self, from: newData) { //Any of this could fail silently at any time
                    self = newObj
                }
            }
        }
    }
}

Something like this will now work

x["fishies"] = 777

print("\(x["fishies"])")

But if there is a type issue

x["fishies"] = "some string"

print("\(x["fishies"])")

You won't know about it.

You can put the subscript in a protocol and reuse it for anything.

struct Server: TerribleProtocol {
    var cats: Int
    var dogs: Int
    var fishies: Int
}
protocol TerribleProtocol: Codable{}

extension TerribleProtocol {
    subscript(_ keyPath: String) -> Any? {