43
votes

How can you get a string value from Swift 4 smart keypaths syntax (e.g., \Foo.bar)? At this point I'm curious about any way at all, does not matter if it's complicated.

I like the idea of type information being associated with smart key path. But not all APIs and 3rd parties are there yet.

There's old way of getting string for property name with compile-time validation by #keyPath(). With Swift 4 to use #keyPath() you have to declare a property as @objc, which is something I'd prefer to avoid.

4
"Expose API to retrieve string representation of KeyPath": bugs.swift.org/browse/SR-5220pkamb

4 Answers

40
votes

A bit late to the party, but I've stumbled upon a way of getting a key path string from NSObject subclasses at least:

NSExpression(forKeyPath: \UIView.bounds).keyPath
35
votes

Short answer: you can't. The KeyPath abstraction is designed to encapsulate a potentially nested property key path from a given root type. As such, exporting a single String value might not make sense in the general case.

For instance, should the hypothetically exported string be interpreted as a property of the root type or a member of one of its nested types? At the very least a string array would need to be exported to address such scenarios...

Per type workaround. Having said that, given that KeyPath conforms to the Equatable protocol, you can provide a custom, per type solution yourself. For instance:

struct Auth {
    var email: String
    var password: String
}
struct User {
    var name: String
    var auth: Auth
}

provide an extension for User-based key paths:

extension PartialKeyPath where Root == User {
    var stringValue: String {
        switch self {
        case \User.name: return "name"
        case \User.auth: return "auth"
        case \User.auth.email: return "auth.email"
        case \User.auth.password: return "auth.password"
        default: fatalError("Unexpected key path")
    }
}

usage:

let name:  KeyPath<User, String> = \User.name
let email: KeyPath<User, String> = \User.auth.email
print(name.stringValue)  /* name */
print(email.stringValue) /* auth.email */

I wouldn't really recommend this solution for production code, given the somewhat high maintenance, etc. But since you were curious this, at least, gives you a way forward ;)

20
votes

For Objective-C properties on Objective-C classes, you can use the _kvcKeyPathString property to get it.

However, Swift key paths may not have String equivalents. It is a stated objective of Swift key paths that they do not require field names to be included in the executable. It's possible that a key path could be represented as a sequence of offsets of fields to get, or closures to call on an object.

Of course, this directly conflicts with your own objective of avoiding to declare properties @objc. I believe that there is no built-in facility to do what you want to do.

15
votes

Expanding on @Andy Heard's answer we could extend KeyPath to have a computed property, like this:

extension KeyPath where Root: NSObject {
    var stringValue: String {
        NSExpression(forKeyPath: self).keyPath
    }
}

// Usage
let stringValue = (\Foo.bar).stringValue
print(stringValue) // prints "bar"