How to Get the Kvc-String from Swift 4 Keypath

Is it possible to get the KVC-String from Swift 4 KeyPath?

I don't know of a pure Swift way to get the name of the property as a string yet.

But, if you add the @objc attribute to the property then _kvcKeyPathString will actually have a value instead of always being nil. Also, since Swift structs can't be represented in Objective-C, this method only works for classes.

A minimal working example usage:

class SomeClass {
@objc var someProperty = 5
}

let keyPath = \SomeClass.someProperty
print(keyPath._kvcKeyPathString)

Getting string from Swift 4 new key path syntax?

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.

How do I use #keyPath() in Swift 4?

According to the docs:

In Objective-C, a key is a string that identifies a specific property of an object. A key path is a string of dot-separated keys that specifies a sequence of object properties to traverse.

Significantly, the discussion of #keyPath is found in a section titled "Interacting with Objective-C APIs". KVO and KVC are Objective-C features.

All the examples in the docs show Swift classes which inherit from NSObject.

Finally, when you type #keyPath in Xcode, the autocomplete tells you it is expecting an @objc property sequence.

Sample Image

Expressions entered using #keyPath will be checked by the compiler (good!), but this doesn't remove the dependency on Objective-C.

Swift 4 new KVC

You need to update to Xcode 9 beta 4, where it is fixed.

How to get a node's value for the key path of a property's property

About the EDIT part.

All properties accessed through KVC (String-based keyPath uses KVC) needs to be exposed to Objective-C runtime. In Swift 4, you need to explicitly annotate with @objc.

class Object: NSObject {
@objc var foo : Foo = Foo() //<-
}

class Foo : NSObject {
@objc var bar : Int = 0 //<-
}

let myObject = Object()
print(myObject.foo.bar) //->0
print(myObject.value(forKeyPath: "foo.bar")) //->Optional(0)

print((myObject.value(forKey: "foo") as! Foo).value(forKey: "bar")) //->Optional(0)
print((myObject.value(forKeyPath: "foo") as! Foo).bar) //->0

About the original part.

Consider using smart KeyPath of Swift 4.

let sprite = SKSpriteNode()
sprite.color = SKColor.red
sprite.size = CGSize(width: 50, height: 50)
sprite.position = CGPoint(x: 320, y: 240)

print(sprite[keyPath: \SKSpriteNode.position]) //->(320.0, 240.0)
print(sprite[keyPath: \SKSpriteNode.size]) //->(50.0, 50.0)
print(sprite[keyPath: \SKSpriteNode.position.x]) //->320.0

Smart KeyPath is not another notation of String-based keyPaths, so it cannot replace all use cases of KVC keyPaths. But it can work with non-Objective-C properties, which is very convenient in some cases.

What is a KeyPath used for?

Objective-C has the ability to reference a property dynamically rather than directly. These references, called keypaths. They are distinct from direct property accesses because they don't actually read or write the value, they just stash it away for use.

Let define a struct called Cavaliers and a struct called Player, then create one instance of each:

// an example struct
struct Player {
var name: String
var rank: String
}

// another example struct, this time with a method
struct Cavaliers {
var name: String
var maxPoint: Double
var captain: Player

func goTomaxPoint() {
print("\(name) is now travelling at warp \(maxPoint)")
}
}

// create instances of those two structs
let james = Player(name: "Lebron", rank: "Captain")
let irving = Cavaliers(name: "Kyrie", maxPoint: 9.975, captain: james)

// grab a reference to the `goTomaxPoint()` method
let score = irving.goTomaxPoint

// call that reference
score()

The last lines create a reference to the goTomaxPoint() method called score. The problem is, we can't create a reference to the captain's name property but keypath can do.

let nameKeyPath = \Cavaliers.name
let maxPointKeyPath = \Cavaliers.maxPoint
let captainName = \Cavaliers.captain.name
let cavaliersName = irving[keyPath: nameKeyPath]
let cavaliersMaxPoint = irving[keyPath: maxPointKeyPath]
let cavaliersNameCaptain = irving[keyPath: captainName]

Please test with Xcode 9 or capable snapshot.

Can the non-string property name passed to #keyPath() be saved independently?

It doesn't look like it is possible.


Here's the compiler's code to parse a key path expression:

///   expr-keypath:
/// '#keyPath' '(' unqualified-name ('.' unqualified-name) * ')'
///
ParserResult<Expr> Parser::parseExprKeyPath() {
// Consume '#keyPath'.
SourceLoc keywordLoc = consumeToken(tok::pound_keyPath);

// Parse the leading '('.
if (!Tok.is(tok::l_paren)) {
diagnose(Tok, diag::expr_keypath_expected_lparen);
return makeParserError();
}
SourceLoc lParenLoc = consumeToken(tok::l_paren);

// Handle code completion.
SmallVector<Identifier, 4> names;
SmallVector<SourceLoc, 4> nameLocs;
auto handleCodeCompletion = [&](bool hasDot) -> ParserResult<Expr> {
ObjCKeyPathExpr *expr = nullptr;
if (!names.empty()) {
expr = ObjCKeyPathExpr::create(Context, keywordLoc, lParenLoc, names,
nameLocs, Tok.getLoc());
}

if (CodeCompletion)
CodeCompletion->completeExprKeyPath(expr, hasDot);

// Eat the code completion token because we handled it.
consumeToken(tok::code_complete);
return makeParserCodeCompletionResult(expr);
};

// Parse the sequence of unqualified-names.
ParserStatus status;
while (true) {
// Handle code completion.
if (Tok.is(tok::code_complete))
return handleCodeCompletion(!names.empty());

// Parse the next name.
DeclNameLoc nameLoc;
bool afterDot = !names.empty();
auto name = parseUnqualifiedDeclName(
afterDot, nameLoc,
diag::expr_keypath_expected_property_or_type);
if (!name) {
status.setIsParseError();
break;
}

// Cannot use compound names here.
if (name.isCompoundName()) {
diagnose(nameLoc.getBaseNameLoc(), diag::expr_keypath_compound_name,
name)
.fixItReplace(nameLoc.getSourceRange(), name.getBaseName().str());
}

// Record the name we parsed.
names.push_back(name.getBaseName());
nameLocs.push_back(nameLoc.getBaseNameLoc());

// Handle code completion.
if (Tok.is(tok::code_complete))
return handleCodeCompletion(false);

// Parse the next period to continue the path.
if (consumeIf(tok::period))
continue;

break;
}

// Parse the closing ')'.
SourceLoc rParenLoc;
if (status.isError()) {
skipUntilDeclStmtRBrace(tok::r_paren);
if (Tok.is(tok::r_paren))
rParenLoc = consumeToken();
else
rParenLoc = PreviousLoc;
} else {
parseMatchingToken(tok::r_paren, rParenLoc,
diag::expr_keypath_expected_rparen, lParenLoc);
}

// If we cannot build a useful expression, just return an error
// expression.
if (names.empty() || status.isError()) {
return makeParserResult<Expr>(
new (Context) ErrorExpr(SourceRange(keywordLoc, rParenLoc)));
}

// We're done: create the key-path expression.
return makeParserResult<Expr>(
ObjCKeyPathExpr::create(Context, keywordLoc, lParenLoc, names,
nameLocs, rParenLoc));
}

This code first creates a list of period-separated names inside the parentheses, and then it attempts to parse them as an expression. It accepts an expression and not data of any Swift type; it accepts code, not data.

Swift KVC setting a NSNumber value to String key

If you know that your keyPath accepts a String, the you could use Swift as? operator to ensure the value you are assigning can be cast to it.
Something like:

self.setValue(value as? String ?? "Bad value", forKeyPath: key)

By doing this, you ensure that you pass the right value for the property (after casting), or a default value in case the case does not succeed.

How to learn about KVC with swift 4.2

In Swift 4 exposing code to the Objective-C runtime is no longer inferred for performance reasons.

To avoid the crash you have to add the @objc attribute explicitly.

@objc var name: String = ""

But from the strong type perspective of Swift there are two better ways to get values with KVC:

  1. The #keyPath directive which uses the ObjC runtime, too, but checks the key path at compile time

    let keyPath = #keyPath(Student.name)
    student1.setValue("Benny", forKeyPath: keyPath)

    In this case you get a very descriptive compiler warning

    Argument of '#keyPath' refers to property 'name' in 'Student' that depends on '@objc' inference deprecated in Swift 4

  2. The (recommended) native way: Swift 4+ provides its own KVC pattern where subclassing NSObject is not required.

    Key paths are indicated by a leading backslash followed by the type and the property (or properties):

    class Student {
    var name: String = ""
    var gradeLevel: Int = 0
    }

    let student1 = Student()
    student1[keyPath: \Student.name] = "Benny"

Is there a way to access an array element in KVC using value(forKeyPath:)?

It's possible with Swift's native KVC even with structs, the ObjC runtime is not needed

struct Person {
var office: Office
var firstName: String
var lastName: String
var reports: [Report]
}

struct Office {
var building: String
var room: Int
}

struct Report {
var title: String
var contents: String
}

let person = Person(office: Office(building: "Main", room: 2 ),
firstName: "Bob", lastName: "Roberts",
reports: [
Report( title: "Today's weather", contents: "..." ),
Report( title: "Traffic", contents: "..." ),
Report( title: "Stocks", contents: "..." )
])

let keyPath = \Person.reports[1].title
let title = person[keyPath: keyPath] // "Traffic"


Related Topics



Leave a reply



Submit