简体   繁体   中英

Storing array of heterogeneous keypaths

I have an object similar to:

class MyObject {
  var title:String? = nil
  var value:Int? = nil
  var otherData:[String] = []
}

I am attempting to store a heterogeneous array of updates so I can use them later to set values. Something like this.

class SomeFormThing {
  func getUpdates() -> [WritableKeyPath<MyObject, Any> : Any] {
    var updates:[WritableKeyPath<MyObject, Any> : Any]
    updates[\MyObject.title] = "New Title"
    updates[\MyObject.value] = 0
    updates[\MyObject.value] = ["Other", "Values"]
    return updates
  }
}

class SyncManager {
  var form:SomeFormThing = SomeFormThing()
  var sync:MyObject = MyObject()
  func updateValues() {
    let updates = form.getUpdates()
    for key in updates.keys {
      sync[forKeyPath: key] = updates[key]
    }
  }
}

However, this of course has a compile problem on the lines in getUpdates() that read Cannot convert value of type 'WritableKeyPath<MyObject, String?>' to expected argument type 'WritableKeyPath<MyObject, Any>' .

The need is to have an array of heterogeneous writable keypaths that I can then use to set values. Given how generics typically work, I don't see why String is not able to equate to the Any generic, since the type is a downcast.

Swift collections don't like to be heterogenous. Moreover, keypaths are generics, for type safety, and generics are not covariant.

So you cannot combine different types in one collection — unless you type-erase. Of course, if you do that, you have to un type erase later, which is a pain in the butt. It's like a roach motel: wrapped types go in but they don't come out unless you elicit them by hand.

For example:

    struct MyObject {
      var title:String
      var value:Int
    }

    // okay, roaches go in...
    let kp1 = \MyObject.title
    let kp2 = \MyObject.value
    let dict : [PartialKeyPath<MyObject>:Any] = [kp1:"Yo", kp2:1]

    var object = MyObject(title: "Ha", value: 0)

    // but now what? how can we apply our `dict` entries to `object`? well...
    for (kp,val) in dict {
        switch (kp,val) {
        case (let kp2 as WritableKeyPath<MyObject,String>, let val2 as String):
            object[keyPath:kp2] = val2
        case (let kp2 as WritableKeyPath<MyObject,Int>, let val2 as Int):
            object[keyPath:kp2] = val2
        default:break
        }
    }
    print(object) // yep, we did it, sort of

So you see, you can do it, but you'd have to deal individually with every possible property type. You've hidden the types in the Partial of PartialKeyPath and in the Any, and now forcing them out into the open is up to you.

That said, the way I would actually do this is to give MyObject a generic update method and just call it one change at a time:

    struct MyObject {
        var title:String
        var value:Int
        mutating func update<T>(_ kp:WritableKeyPath<Self,T>, to val:T) {
            self[keyPath:kp] = val
        }
    }

And so:

    object.update(\.title, to: "Teehee")
    object.update(\.value, to: 42)

Note that you cannot do that directly from an entry in the dict because you've hidden the types. A generic doesn't wait until runtime to look to see what type something "really" is; it is resolved by the compiler, so the type must be known — and by saying PartialKeyPath and Any you have made the type un known.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM