简体   繁体   中英

Swift - KeyPath extension - enforce conformance to protocol

ideally, I'd like to get the name of the property referenced by a KeyPath. But this seems not to be possible out-of-the-box in Swift.

So my thinking is that the KeyPath could provide this information based on protocol extension added by a developer. Then I'd like to design an API with an initializer/function that accepts a KeyPath conforming to that protocol (that adds a computed property).

So far I was only able to define the protocol and conditional conformance of the protocol. The following code compiles fine.

protocol KeyPathPropertyNameProviding {
    var propertyName: String {get}
}

struct User {
    var name: String
    var age: Int
}

struct Person  {
    var name: String
    var age: Int
}

extension KeyPath: KeyPathPropertyNameProviding where Root == Person {
    var propertyName: String {
        switch self {
            case \Person.name: return "name"
            case \Person.age: return "age"
            default: return ""
        }
    }
}

struct PropertyWrapper<Model> {
    var propertyName: String = ""
    init<T>(property: KeyPath<Model, T>) {
        if let property = property as? KeyPathPropertyNameProviding {
            self.propertyName = property.propertyName
        }
    }
}

let userAge = \User.age as? KeyPathPropertyNameProviding
print(userAge?.propertyName) // "nil"
let personAge = \Person.age as? KeyPathPropertyNameProviding
print(personAge?.propertyName) // "age"

let wrapper = PropertyWrapper<Person>(property: \.age)
print(wrapper.propertyName) // "age"

But I am unable to restrict the API so that initialization parameter property has to be a KeyPath AND must conform to a certain protocol.

For example, the following would result in a compilation error but should work from my understanding (but probably I miss a key detail ;) )

struct PropertyWrapper<Model> {
    var propertyName: String = ""
    init<T>(property: KeyPath<Model, T> & KeyPathPropertyNameProviding) {
        self.propertyName = property.propertyName // compilation error "Property 'propertyName' requires the types 'Model' and 'Person' be equivalent"
    }
}

Any tips are highly appreciated!

You are misunderstanding conditional conformance. You seem to want to do this in the future:

extension KeyPath: KeyPathPropertyNameProviding where Root == Person {
    var propertyName: String {
        switch self {
            case \Person.name: return "name"
            case \Person.age: return "age"
            default: return ""
        }
    }
}

extension KeyPath: KeyPathPropertyNameProviding where Root == User {
    var propertyName: String {
        ...
    }
}

extension KeyPath: KeyPathPropertyNameProviding where Root == AnotherType {
    var propertyName: String {
        ...
    }
}

But you can't. You are trying to specify multiple conditions to conform to the same protocol. See here for more info on why this is not in Swift.

Somehow, one part of the compiler thinks that the conformance to KeyPathPropertyNameProviding is not conditional, so KeyPath<Model, T> & KeyPathPropertyNameProviding is actually the same as KeyPath<Model, T> , because somehow KeyPath<Model, T> already "conforms" to KeyPathPropertyNameProviding as far as the compiler is concerned, it's just that the property propertyName will only be available sometimes .

If I rewrite the initialiser this way...

init<T, KeyPathType: KeyPath<Model, T> & KeyPathPropertyNameProviding>(property: KeyPathType) {
    self.propertyName = property.propertyName
}

This somehow makes the error disappear and produces a warning:

Redundant conformance constraint 'KeyPathType': 'KeyPathPropertyNameProviding'

Key paths are hashable, so I recommend a dictionary instead. It's especially easy to put it together with strong typing if you're able to use a CodingKey type.

struct Person: Codable  {
  var name: String
  var age: Int

  enum CodingKey: Swift.CodingKey {
    case name
    case age
  }
}

extension PartialKeyPath where Root == Person {
  var label: String {
    [ \Root.name: Root.CodingKey.name,
      \Root.age: .age
    ].mapValues(\.stringValue)[self]!
  }
}

Then use parentheses instead of the cast you demonstrated. No need for a protocol so far…

(\Person.name).label // "name"
(\Person.age).label // "age"

This will probably all be cleaner due to built-in support someday. https://forums.swift.org/t/keypaths-and-codable/13945

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