What Is a Keypath Used For

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.

What's the purpose of keyPath in IDBObjectStore.createIndex()?

A great way to understand things is to think how you would design it yourself.

Let's take a step back, and look at how a very simple NoSQL database would store data, then add indices.

Each Store would look like a JavaScript object (Or dictionary in python, a hash in C# ) etc...

{
"1": {"name": "Lara", "age": "20"},
"2": {"name": "Sara", "age": "22"},
"3": {"name": "Joe", "age": "22"},
}

This structure is basically a list of values, plus an index to retrieve the values, like so:

//Raw JS:
var sara = data["2"]
// IndexedDB equivalent:
store.get(2)

This is super fast for direct object retrieval, but sucks for filtering. If you want to get people of a specific age, you need to loop over every value.

If you knew in advance you would be doing your queries by age, you could really speed up such queries by creating a new object which indexes the value by age:

{
"20": [<lara>],
"22": [<joe>, <sara>],
...
}

But how would you query that? You can't use the default indexing syntax (e.g. data[22] or store.get(22)) because those expect the key.

One way would be to name that second index, e.g. by_age_index and give our store object a way to access that index by name, so you could to this:

store.index('by_age_index').get(22) // Returns Joe and Sara

The last bit of the puzzle would be telling that index how to determine which records go against which key (because it has to keep itself updated when records are added/changed/removed)

In other words, how does it know Joe and Sara go against key 22, but Lara goes against key 20?

In our case, we want to use the field age from each record. This is what we mean by a keyPath.

So when defining an index, it makes sense that we would specify that as well as the name, e.g.

store.createIndex('by_age_index', 'age');

Of course, if you want to access your index like this:

store.index('age').get(22) // Returns Joe and Sara

Then you need to create your index like this:

store.createIndex('age', 'age');

Which is what most people do, which is why we see that in examples, which gives the impression that the first argument is the keyPath (whereas it's actually just the arbitrary name we give that index) leaving us unsure about what the second argument might be for.

I could have explained all this by saying:

The first parameter is the handle by which you access the index on the store, the second parameter is the name of the field on the record by which that index should group its records.

But maybe this rundown will help other people too :-)

what's the difference between single key and keypath?

A key is a string that identifies a property of an object. A key path is a list of keys separated by dots, used to identify a nested property.

Here's an example. If an object person has a property address, which itself has a property town you could get the town value in two steps using keys:

id address = [person valueForKey:@"address"];
id town = [address valueForKey:@"town"];

or in one step using a keyPath:

id town = [person valueForKeyPath:@"address.town"];

Have a look at Apple's docs on Key-Value Coding for further details.

What is the use of #keyPath rather than directly passing key as a String?

There is no difference between

key:"dateCreated"

and

key: #keyPath(Note.dateCreated)

both will do the sort with Note object's dateCreated property
the latter has an advantage of avoiding hard coding problems e.x writing datCreated instead of dateCreated will throw a compile time error , so it'll safely avoid run-time crashes that definitely will happen with the former under same circumstances

https://www.klundberg.com/blog/swift-4-keypaths-and-you/

http://chris.eidhof.nl/post/sort-descriptors-in-swift/

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 keyPath vs protocol

If you already know the type of the instance, you can access their properties easily. If you don’t, protocol already supports read-only, read-write properties. Can someone explain me what I am missing?

What you're missing is a sense of what's unknown.

In both your sentences you speak of knowing what the instance's properties are. That's not the problem key paths solve. Key paths have nothing to do with knowing the type; they are not in any kind of opposition to types or protocols. On the contrary, before you can use a key path, you have to know exactly what the instance's properties are.

Key paths are for when what is unknown is which property to access. They provide a way to pass a property reference so that someone else can be told to access that property.

For example, here's a Person type:

struct Person {
let firstName : String
let lastName : String
}

And here is a function that sorts an array of Persons by either the firstName or the lastName, without knowing which one to sort by:

func sortArrayOfPersons(_ arr:[Person], by prop: KeyPath<Person, String>) -> [Person] {
return arr.sorted { $0[keyPath:prop] < $1[keyPath:prop] }
}

A key path is how you tell this function what property to use as a basis for sorting.

how do you write a KeyPath to reference the value of a dictionary

Your question is unclear because you refer to User in your text, but have Person in your code. That suggests that you might have the syntax right but a hierarchy wrong.

You also may just be forgetting the question mark.

To get the type you're asking about, you also need a KidID (the D is capitalized in Swift) value, not the KidID type.

typealias KidID = String
enum KidName { case goatBoy }
\Person.kids["quot;] as WritableKeyPath<Person, KidName?>
Person(kids: ["quot;: .goatBoy])[keyPath: \.kids["quot;]] // .goatBoy

You don't have to supply a value in the key path, but that is a different type.

\Person.kids as WritableKeyPath<Person, [KidID: KidName]>
Person(kids: ["quot;: .goatBoy])[keyPath: \.kids]["quot;] // .goatBoy

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.

map(keyPath) where keyPath is a variable

Array.map expects a closure with signature (Element) throws -> T.

In Swift 5.2, key paths were allowed to be passed in as functions/closures (here's an evolution proposal), but only as literals (at least, according to the proposal, it says "for now", so perhaps this restriction would be lifted).

To overcome this, you can create an extension on Sequence that accepts a key path:

extension Sequence {
func map<T>(_ keyPath: KeyPath<Element, T>) -> [T] {
return map { $0[keyPath: keyPath] }
}
}

(credit to: https://www.swiftbysundell.com/articles/the-power-of-key-paths-in-swift/)

Then you can do what you wanted:

let keyPath = \(Int, Int).0
arr.map(keyPath)


Related Topics



Leave a reply



Submit