Dynamic swift - Part 1: KeyPath
Swift KeyPath Estimated reading time: 12 minutes- dynamic or static?
- keyPath
- AnyKeypath
- PartialKeyPath
- KeyPath
- WritableKeyPath
- ReferenceWritableKeyPath
- Bonus
- Resources
Dynamic features bring some flexibility and additional functionality into the programming language, but at the same moment, this can reduce compile-time safety.
Swift has a strongly-typed system. In the same moment during the last few years, a few dynamic features were added to it. The most interesting are:
Also, it’s good to mention about opposite to @dynamicCallable - static Callable
Some dynamic features were present in language from the very beginning:
Mirror
.- additions from Obj-C (such as KVO, dynamic dispatch, etc) (this still can be used by using
@objc
,dynamic
,NSObject
and other modifiers/techniques)
Related articles:
- Dynamic swift - Part 1: KeyPath
- Dynamic swift - Part 2: @dynamicMemberLookup
- Dynamic swift - Part 3: Opposite to @dynamicCallable - static Callable
- Dynamic swift - Part 4: @dynamicReplacement
dynamic or static?
Swift is a static type (or strong type) language - this means, that compiler should now all information about all classes, functions, and other types. This means, that all features, that somehow “close the all-seeing eye” of a compiler, can be named as a dynamic feature.
For comparison let’s think about Obj-C - in this language all things are a bit more flexible than in Swift. This is because compiler deferring the bounding process for the all of components as long as it’s possible, as result - u get a great flexibility and a wide possibilities (dynamic typing, dynamic binding, kvc, kvo, forward invokation, swizzling, etc).
The downside of such a process is safety. And Apple knows it.
source from iOS 3 dump of ViewController or wiki
Swift started obtaining a different dynamic feature when support for Linux was added. On Linux, there is no Foundation
framework from Apple (at least at the beginning, for now, it’s open-source), so runtime features from Obj-C are not available, but developers still need it, and open-source Swift allows them to do this.
As of now, we have a few of them already added, and a few more are waiting for proposals.
keyPath
KeyPath - A key path from a specific root type to a specific resulting value type.
KeyPath allows us to get access to type variables using strongly-typed paths that are checked at compilation time.
There are few types of keyPath:
AnyKeyPath
- a type-erased key path, from any root type to any resulting value typePartialKeyPath
- a partially type-erased key path, from a concrete root type to any resulting value type.KeyPath
: read-only.WritableKeyPath
: read-write.ReferenceWritableKeyPath
: read-write, mutable, only for ref types.
mentioned according to a class hierarchy
It’s also important to understand, that #keyPath
added in Swift 3 compiles down into a String. It was added mostly to support Obj-C KVO/KVC and so, requires that our object inherits from NSObject
. This off cause introduces some constraints into Swift code.
Such an approach also does not provide any information about the type of object to which it below - it’s just a String. This means, that we can’t work with data processed with a key path as with concrete type data - only as Any
.
KeyPath
type, instead, is a fast, property traversal, statically type-safe, available for all values and on all platforms where Swift works.
usage of keyPath
subscript also allowed.
AnyKeypath
AnyKeyPath
does not have some generic constraints, and this type is created for handling any keypath (:]) from any objects. So this type is fully erased, and don’t know about its route and root object.
route
means expression, that describes how we can get this property from a specific (or unknown) object. Another name for this object -root
AntKeyPath
is used when u have a deal with many keypaths from different objects. This can be an array of items, or input to u’r functions:
In addition to described above, this type conforms to protocol _AppendKeyPath
- this protocol allows us to modify keyPath by appending some components.
note: Apple mentions in header “do not use this protocol directly.”
To describe rules of appending here is a quick image:
When we perform appending of different keyPath, we may receive different KeyPath class as result:
First | Second | Result |
---|---|---|
AnyKeyPath | Anything | AnyKeyPath? |
PartialKeyPath | AnyKeyPath or PartialKeyPath | PartialKeyPath? |
PartialKeyPath | KeyPath or WritableKeyPath | KeyPath? |
PartialKeyPath | ReferenceWritableKeyPath | ReferenceWritableKeyPath? |
KeyPath | AnyKeyPath or PartialKeyPath | 💥 Not possible 💥 |
KeyPath | KeyPath or WritableKeyPath | KeyPath |
KeyPath | ReferenceWritableKeyPath | ReferenceWritableKeyPath |
WritableKeyPath | AnyKeyPath or PartialKeyPath | 💥 Not possible 💥 |
WritableKeyPath | KeyPath | KeyPath |
WritableKeyPath | WritableKeyPath | WritableKeyPath |
WritableKeyPath | ReferenceWritableKeyPath | ReferenceWritableKeyPath |
ReferenceWritableKeyPath | AnyKeyPath or PartialKeyPath | 💥 Not possible 💥 |
ReferenceWritableKeyPath | KeyPath | KeyPath |
ReferenceWritableKeyPath | WritableKeyPath or ReferenceWritableKeyPath | ReferenceWritableKeyPath |
source of this comparison table is described by Kevin Lundberg
If we looked into the header we could also find, that we have few more available public properties: a few required by a Hashable
protocol, and also 2 more, that describe root
type and value
type:
If we check source code for KeyPath
in swift, we can observe, that this values use _rootAndValueType
value, that is implemented as follow:
where _abstract()
defined as:
For generic subclasses this method has a bit different implementation:
Indeed, if we try to grab this 2 values from AnyKeyPath
, we will get an fatalError:
But for types that is inherit from KeyPath
(including), value about this types become available and we can obtain it easelly:
Why do this vaiables exists? This information used when different operations is tried to be performed within selected KeyPath. For example - appending, when we want to do this, on some internal step of this process:
PartialKeyPath
PartialKeyPath
- A partially type-erased key path, from a concrete root type to any resulting value type.
More about idea - read here.
If we check implementation, we can see next:
Not much, the only difference is that this type holds information about the Root
type. As result, this type knows about its base type:
This type of keyPath is also read-only.
KeyPath
KeyPath
. This is a type, that describes not just a Root object, but also a value type.
For KeyPath
we can check valueType
and rootType
:
But the most important - we have now information about valueType. Example of usage may be next:
or u may want to use it in some logic like next:
WritableKeyPath
WritableKeyPath
This is the same as the previous type, but allows perform write in addition to read. A good note about this key-path - it’s only for value-type.
To explain this type it’s better to provide an example:
Output:
If we check type of someKeyPath
:
As I mentioned before, KeyPath
- read-only. Now, let’s modify our Some
struct:
u also could resolve this issue by adding mutation keyword to a method that calls this keypath, but this is not the right decision.
If we now check someKeyPath
:
Now, operations like someVar[keyPath: someKeyPath] = 2
allowed.
Example of usage:
I will tell more about
dynamicMemberLookup
in the next article
Sometimes u can omit
ReferenceWritableKeyPath
ReferenceWritableKeyPath
. Same as the previous one, but now for a reference type. So this type was created specifically to support reading from and writing to the resulting value using reference semantics.
If we compare WritableKeyPath
vs ReferenceWritableKeyPath
- the fists one writes directly into a value-type base (inout or mutating), the second one writes into a reference-type base (simply invoking setter on reference type).
Bonus
value observation
We also can use now keyPath for value obsevation:
SE-0249
In proposal SE-0249 described ideat that can add to key paths the ability to be used as functions.
Now this is implemented and can be used within Combine
:
let responsePublisher = publisher
.map(\.data)
// ^~~~~~~~~
.decode(type: FeedItem.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
Obj-C #keyPath
It’s also good to mention, that we still can use #keyPath
for Objective-C code.
In the next articles, I will cover other dynamic aspects of Swift.
Related articles:
- Dynamic swift - Part 1: KeyPath
- Dynamic swift - Part 2: @dynamicMemberLookup
- Dynamic swift - Part 3: Opposite to @dynamicCallable - static Callable
- Dynamic swift - Part 4: @dynamicReplacement
Resources
- Key-Path Expressions
- Key Paths
- Dynamic member lookup
- Key Path Member Lookup
- Dynamic replacement
- Callable
- The Objective-C Runtime & Swift Dynamism
- WWDC 17 #212 What’s New in Foundation
- Swift 4 KeyPaths and You
- Referencing Objective-C key-paths
- Key Path Expressions as Functions
- Using Key-Value Observing in Swift
Share on: