Getting string from Swift 4 new key path syntax?
Asked Answered
D

7

58

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.

Desai answered 2/10, 2017 at 15:46 Comment(1)
"Expose API to retrieve string representation of KeyPath": bugs.swift.org/browse/SR-5220Largo
H
27

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.

Hubert answered 4/10, 2017 at 1:47 Comment(2)
Nice catch! I did a quick test here and, unfortunately, it looks like the aforementioned _kvcKeyPathString property will only return non-nil values for @objc exposed properties. For instance, both name and email key paths, in my answer above, returned in nil.Quartering
Note from a related question: >> But, if you add the @objc attribute to the property then _kvcKeyPathString will actually have a value instead of always being nil. << #46143792Largo
R
55

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
Rubbing answered 17/7, 2018 at 8:37 Comment(6)
Nice idea, though I get a crash when running this in swift REPL import Foundation; class AClass: NSObject { var aValue: Int! }; NSExpression(forKeyPath: \AClass.aValue).keyPathDesai
@DannieP I think that would be because your property is of Int! type, which is an optional integer which cannot be exposed to Objective C. An Int type would probably work though.Rubbing
I've just tried and it's pretty much the same for 'Int' and 'NSNumber".Desai
@DannieP You need to expose the property to Objective-C using @objc. I tested your sample code (using a non-optional Int) in a Swift Playground and it worked just fine after doing that ;)Quartering
And that's something I'd still prefer to avoid. Thanks for the tip!Desai
This seems to crash when _kvcKeyPathString is nil, but at least it is a non-_underscoreFunction alternative in the public API.Largo
Q
36

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-ish 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 ;)

Quartering answered 4/10, 2017 at 1:19 Comment(3)
How about using Mirror.Children (get a Child) to recursive printing a path using dot notation. What i cant believe is that mirror and KeyPath dont work with each other... what a missed opportunity on apple behalf.Eliason
@RicardoDuarte Mirror is a black-box, and nothing is guaranteed about its return value. Therefore, if you involve Mirror in your logic, your logic can break later if Apple changes Mirror implementation, and such breakage would be very very hard to fix if happens because direction of changes is unpredictable.Poach
you can use sourcery to generate this boilplates. it is easy to maintainSluggish
H
27

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.

Hubert answered 4/10, 2017 at 1:47 Comment(2)
Nice catch! I did a quick test here and, unfortunately, it looks like the aforementioned _kvcKeyPathString property will only return non-nil values for @objc exposed properties. For instance, both name and email key paths, in my answer above, returned in nil.Quartering
Note from a related question: >> But, if you add the @objc attribute to the property then _kvcKeyPathString will actually have a value instead of always being nil. << #46143792Largo
V
23

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"

Vlissingen answered 11/10, 2019 at 17:40 Comment(0)
I
3

I needed to do this recently and I wanted to ensure that I get a static type check from the compiler without hardcoding the property name.

If your property is exposed to Objective-C(i.e @objc), you can use the #keyPath string expression. For example, you can do the following:

#keyPath(Foo.bar)
#keyPath(CALayer.postion)

See Docs

Interstellar answered 24/11, 2022 at 9:54 Comment(0)
B
3

Quite late to the party, but this was my solution in Swift:

extension PartialKeyPath {
    var keyPath: String {
        let me = String(describing: self)
        let rootName =  String(describing: Root.self)
        let removingRootName = me.components(separatedBy: rootName)
        var keyPathValue = removingRootName.last ?? ""
        if keyPathValue.first == "." { keyPathValue.removeFirst() }
        return keyPathValue
    }
}

Alternatively, since I believe String(describing: KeyPath) always structures the string as "\RootName.keyPathName", you could use dropFirst instead:

extension PartialKeyPath {
    var keyPath: String {
        let me = String(describing: self)
        let dropLeading =  "\\" + String(describing: Root.self) + "."
        let keyPath = "\(me.dropFirst(dropLeading.count))"
        return keyPath
    }
}
Brigandine answered 24/4, 2023 at 16:26 Comment(2)
Nope. String(describing: self) returns a description on Value type instead of keyPathNameTrismus
This will break if your model is in a module, or uses a WritableKeyPath instead of a regular KeyPath.Symposium
H
1

I created this extension for getting property names. Also works as part of packages.

extension KeyPath {
    var propertyAsString: String {
        "\(self)".components(separatedBy: ".").last ?? ""
    }
}
Henrik answered 14/10, 2023 at 2:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.