Every kind and what decides it, every step a path can take, and how one is built and used — in SIL, in LLVM IR, and in the source of the standard library, the optimizer and SwiftUI
Lesson 13 met key paths from four sides: a property as a value and the three kinds (steps 2 and 3), map(\.grower) and KeyPathComparator (step 4), the object inside (step 6), and @dynamicMemberLookup (step 7). This guide goes all the way in. First, what a key path can be: every kind, and what decides yours; every step a path can take; when two are equal, and when they are the same object. Then how one is built and used, at every level there is: the pattern the compiler writes, the object the runtime makes from it, what that object holds, how it knows which property it reads — and what stands in for an offset the compiler cannot know — and what a read or a write through it becomes — read in SIL, in LLVM IR, and in the source of the standard library and of the optimizer. Last, @dynamicMemberLookup in full, the places SwiftUI takes a key path, and what a key path cannot reach. Every piece of code on this page was compiled and run on the iOS 27 simulator, and every output, error and line of compiler output shown is what that run really gave.
[keyPath:] compiles to · lesson 13@dynamicMemberLookup, in full · lesson 13Before Swift 4, a key path was a string. Swift 3 added #keyPath (SE-0062), which checks a property's name when the code compiles and then hands you the name, for Objective-C's key-value coding. It works only on @objc properties of classes that inherit from NSObject:
import Foundation
final class Place: NSObject {
@objc var name = "Seattle"
}let path = #keyPath(Place.name)
print(path, type(of: path))
print(Place().value(forKeyPath: path) as Any)It prints
name String
Optional(Seattle)The path is the string "name" and nothing more. value(forKeyPath:) looks the name up while the program runs, and returns Any?: the property's type is lost on the way.
Swift 4 (SE-0161) made a key path a value of its own, whose type names both of its ends: \Trip.city leads from a Trip to a String. You write the root type after the backslash, or leave it out where the context already says it — in trips.map(\.city), map is called on an array of trips. The guide uses this struct throughout:
struct Trip {
let id: Int
var city: String
var nights: Int
var stops: [String] = []
var note: String? = nil
}A key path is used through the subscript [keyPath:], which every type has without declaring it: trip[keyPath: \Trip.city] reads, and assigning to it writes. Lesson 13's step 2 did both.
public class AnyKeyPath : _AppendKeyPath { … }
public class PartialKeyPath<Root> : AnyKeyPath { … }
public class KeyPath<Root, Value> : PartialKeyPath<Root> { … }
public class WritableKeyPath<Root, Value> : KeyPath<Root, Value> { … }
public class ReferenceWritableKeyPath<Root, Value> : WritableKeyPath<Root, Value> { … }Each class adds something to the one above it. An AnyKeyPath knows neither of its ends at compile time. Reading through one gives Any? — and nil when the root has the wrong type — though the object itself still knows both ends:
let any: AnyKeyPath = \Trip.city
let trip = Trip(id: 1, city: "Seattle", nights: 5)
print(trip[keyPath: any] as Any)
print("Seattle"[keyPath: any] as Any)
print(type(of: any).rootType, type(of: any).valueType)It prints
Optional("Seattle")
nil
Trip StringA PartialKeyPath<Root> knows the root but not the value, which is what lets paths to different properties share one array or one set — lesson 13 put \.weight and \.girth in a Set<PartialKeyPath<Pumpkin>>. A KeyPath knows both, and reads. A WritableKeyPath also writes, into a value held in a var. A ReferenceWritableKeyPath writes through a reference, so a let will do.
You never choose the kind. The compiler gives a key path the most capable kind its whole route allows, and the route is made of declarations. Here is one declaration of each sort:
struct Trip {
let id: Int
var city: String
var shout: String {
city.uppercased()
}
var nickname: String {
get {
city
}
set {
city = newValue
}
}
var label: String {
get {
"Trip \(id)"
}
nonmutating set {
print("not kept:", newValue)
}
}
static var made = 0
}
final class Store {
let name = "mine"
var trip = Trip(id: 1, city: "Seattle")
}
struct Holder {
let store: Store
}print(type(of: \Trip.id))
print(type(of: \Trip.city))
print(type(of: \Trip.shout))
print(type(of: \Trip.nickname))
print(type(of: \Trip.label))
print(type(of: \Store.name))
print(type(of: \Store.trip.city))
print(type(of: \Holder.store.trip.city))
print(type(of: \Trip.Type.made))It prints
KeyPath<Trip, Int>
WritableKeyPath<Trip, String>
KeyPath<Trip, String>
WritableKeyPath<Trip, String>
ReferenceWritableKeyPath<Trip, String>
KeyPath<Store, String>
ReferenceWritableKeyPath<Store, String>
ReferenceWritableKeyPath<Holder, String>
ReferenceWritableKeyPath<Trip.Type, Int>| The path | Its kind | Because |
|---|---|---|
\Trip.id | KeyPath | id is a let |
\Trip.city | WritableKeyPath | a var: writing changes the struct, so the struct must be in a var |
\Trip.shout | KeyPath | a computed property with no setter |
\Trip.nickname | WritableKeyPath | a computed property with a setter. A struct's setter is mutating: it changes the struct |
\Trip.label | ReferenceWritableKeyPath | a nonmutating set changes nothing inside the struct, so the struct may be a constant |
\Store.name | KeyPath | a let, in a class |
\Store.trip.city | ReferenceWritableKeyPath | the first step is a property of an object; the write lands inside the object |
\Holder.store.trip.city | ReferenceWritableKeyPath | Holder is a struct, but store holds a reference: the write lands in the object, and the Holder does not change |
\Trip.Type.made | ReferenceWritableKeyPath from Trip.Type | a static property: the root is the type itself (SE-0438, Swift 6.1) |
So reference-writable does not mean "a class". It means that writing does not change the root value itself, so the root may be a constant — and a class's property, a nonmutating set and anything reached through a reference all qualify. A let of a struct will do:
let holder = Holder(store: Store())
holder[keyPath: \Holder.store.trip.city] = "Victoria"
print(holder.store.trip.city)It prints
VictoriaThere is one more input to the kind: where the path is written. A private(set) property is writable inside its own file and read-only outside it, and so is a key path to it — Inside, part 1 shows how far that goes.
A key path is a chain of steps — the standard library calls them components. Besides stored and computed properties, a step can be a subscript, an optional, a tuple element, or nothing at all:
struct Spot {
var name: String
var coords: (lat: Double, lon: Double)
var label: String {
get {
name.uppercased()
}
set {
name = newValue
}
}
}var trip = Trip(id: 1, city: "Seattle", nights: 5, stops: ["Tacoma", "Olympia"], note: "window seat")
print(trip[keyPath: \Trip.stops[1]])
trip[keyPath: \Trip.stops[0]] = "Portland"
print(trip.stops)
print(trip[keyPath: \Trip.note?.count] as Any)
trip[keyPath: \Trip.note!] = "aisle"
print(trip[keyPath: \Trip.self].note!)
let nights = ["Seattle": 5]
print(nights[keyPath: \[String: Int].["Seattle"]] as Any)
var spot = Spot(name: "Pier", coords: (lat: 47.6, lon: -122.3))
spot[keyPath: \Spot.coords.lat] = 48.4
print(spot[keyPath: \Spot.coords.0], spot[keyPath: \Spot.coords.1])It prints
Olympia
["Portland", "Olympia"]
Optional(11)
aisle
Optional(5)
48.4 -122.3print(type(of: \Trip.stops[0]))
print(type(of: \Trip.note?.count))
print(type(of: \Trip.note!))
print(type(of: \Trip.self))
print(type(of: \[String: Int].["Seattle"]))
print(type(of: \Spot.coords.lat))It prints
WritableKeyPath<Trip, String>
KeyPath<Trip, Optional<Int>>
WritableKeyPath<Trip, String>
WritableKeyPath<Trip, Trip>
WritableKeyPath<Dictionary<String, Int>, Optional<Int>>
WritableKeyPath<Spot, Double>| Step | Written | What it does to the path |
|---|---|---|
| A subscript | \Trip.stops[1], \[String: Int].["Seattle"] | Writable if the subscript has a setter. A dictionary's subscript gives an optional, nil for a missing key |
| Optional chaining | \Trip.note?.count | The path becomes read-only, and its value optional: a nil ends it |
| Force-unwrapping | \Trip.note! | The path stays writable — and, as ! does anywhere, stops the program if the value is nil |
| A tuple element | \Spot.coords.lat, \Spot.coords.0 | By label or by position; writable, like a stored property |
| Nothing | \Trip.self | The identity key path (SE-0227, Swift 5): the whole value. Writable |
A subscript's arguments become part of the path, and that comes with a condition:
struct Place {
var name: String
}
struct Atlas {
subscript(place: Place) -> String {
place.name.uppercased()
}
}let seattle = \Atlas[Place(name: "Seattle")]It does not compile
subscript index of type 'Place' in a key path must be Hashable
Make Place Hashable, and it compiles. Two key paths are compared step by step, so \Atlas[seattle] and \Atlas[victoria] must be told apart by their arguments — and a key path must be hashable as a whole. Inside, part 1 shows the two functions the compiler stores with such a path to compare its arguments and hash them.
Paths can also be joined. appending(path:) adds a path that starts where the first one ends:
let stops = \Trip.stops
let count = stops.appending(path: \[String].count)
let first = stops.appending(path: \[String].[0])
print(type(of: count))
print(type(of: first))
let trip = Trip(id: 1, city: "Seattle", nights: 5, stops: ["Tacoma"])
print(trip[keyPath: count], trip[keyPath: first])It prints
KeyPath<Trip, Int>
WritableKeyPath<Trip, String>
1 TacomaThe joined path can write only if both halves can. \[String].count cannot, so count is a KeyPath; \[String].[0] can, so first is writable.
Since Swift 5.2 (SE-0249), a key path literal can stand where a function is expected — lesson 13's map(\.grower). The key path does not turn into a function. The compiler writes a closure around it:
let trips = [Trip(id: 1, city: "Seattle", nights: 5), Trip(id: 2, city: "Victoria", nights: 1)]
print(trips.map(\.city))It prints
["Seattle", "Victoria"]// static Program.main()
%63 = keypath $KeyPath<Trip, String>, (root $Trip; stored_property #Trip.city : $String)
// function_ref implicit closure #1 in static Program.main()
%64 = function_ref @$s7program7ProgramV4mainyyFZSSAA4TripVcfu_ : $@convention(thin) @substituted <τ_0_0, τ_0_1, τ_0_2> (@in_guaranteed τ_0_0, @guaranteed KeyPath<Trip, String>) -> (@out τ_0_2, @error_indirect τ_0_1) for <Trip, Never, String>
%66 = partial_apply [callee_guaranteed] [on_stack] %64(%63) : $@convention(thin) @substituted <τ_0_0, τ_0_1, τ_0_2> (@in_guaranteed τ_0_0, @guaranteed KeyPath<Trip, String>) -> (@out τ_0_2, @error_indirect τ_0_1) for <Trip, Never, String>
// implicit closure #1 in static Program.main()
%11 = function_ref @swift_getAtKeyPath : $@convention(thin) <τ_0_0, τ_0_1> (@in_guaranteed τ_0_0, @guaranteed KeyPath<τ_0_0, τ_0_1>) -> @out τ_0_1
%12 = apply %11<Trip, String>(%0, %9, %3) : $@convention(thin) <τ_0_0, τ_0_1> (@in_guaranteed τ_0_0, @guaranteed KeyPath<τ_0_0, τ_0_1>) -> @out τ_0_1main builds the key path (keypath) and makes a closure that captures it (partial_apply of implicit closure #1). The closure takes one trip and reads through the key path with swift_getAtKeyPath, the standard library's function for [keyPath:] — more about it in What [keyPath:] compiles to. So trips.map(\.city) is trips.map { $0[keyPath: \Trip.city] }, written for you.
Only a literal gets that treatment. A key path held in a variable is an object, not a function:
let trips = [Trip(id: 1, city: "Seattle", nights: 5)]
let city = \Trip.city
let cities = trips.map(city)It does not compile
no exact matches in call to instance method 'map'
note: found candidate with type '((Trip) throws(E) -> T) throws(E) -> [T]'
The note shows what map wanted: a function from Trip. Write the closure yourself — trips.map { $0[keyPath: city] }.
Key paths are Hashable. Two paths are equal when they take the same steps, however they were made. Whether they are the same object is another question:
let stops = \Trip.stops
let joined = stops.appending(path: \[String].count)
print(joined == \Trip.stops.count, joined === \Trip.stops.count)
print(\Trip.city === \Trip.city)
print(\Trip.stops[0] == \Trip.stops[0], \Trip.stops[0] === \Trip.stops[0])
print(\Trip.stops[0] == \Trip.stops[1])
let labels: [PartialKeyPath<Trip>: String] = [\Trip.city: "City", \Trip.nights: "Nights"]
let trip = Trip(id: 1, city: "Seattle", nights: 5)
for path in [\Trip.nights, \Trip.city] as [PartialKeyPath<Trip>] {
print(labels[path]!, trip[keyPath: path])
}It prints
true false
true
true false
false
Nights 5
City SeattleThe path made with appending equals the literal \Trip.stops.count, but it is a different object. The literal \Trip.city, written twice in one function, is one object: the runtime makes it once and hands it back after that (Inside, part 1 says when that stops being true). A path with a subscript is made afresh each time the expression runs, even with a literal 0, so the two copies are equal and not the same object. Different arguments make different paths.
Because they are Hashable, key paths make good dictionary keys: the loop found each label by its path. Observation uses them that way. The code @Observable writes (lesson 16) reports each read and each change to the registrar under the property's key path — access(keyPath: \.trips) in the getter, withMutation(keyPath: \.trips) in the setter.
Lesson 13's step 6 read the first step out of two objects. Here, in SIL — the language the compiler works in before it makes machine code — is the pattern the compiler writes for one of each kind of step — a subscript with an argument, an optional chain, a forced unwrap, a tuple element, a computed property with a setter, and a path through an object:
final class Store {
var trip = Trip(id: 1, city: "Seattle", nights: 5)
}let i = 1
let stop = \Trip.stops[i]
let letters = \Trip.note?.count
let note = \Trip.note!
let lat = \Spot.coords.lat
let label = \Spot.label
let city = \Store.trip.city
print(stop, letters, note, lat, label, city)// static Program.main()
%5 = keypath $WritableKeyPath<Trip, String>, (root $Trip; stored_property #Trip.stops : $Array<String>; settable_property $String, id @$sSayxSicig : $@convention(method) <τ_0_0> (Int, @guaranteed Array<τ_0_0>) -> @out τ_0_0, getter @$sSayxSicipSaySSGSSTK : $@convention(keypath_accessor_getter) (@in_guaranteed Array<String>, @in_guaranteed Int) -> @out String, setter @$sSayxSicipSaySSGSSTk : $@convention(keypath_accessor_setter) (@in_guaranteed String, @inout Array<String>, @in_guaranteed Int) -> (), indices [%$0 : $Int : $Int], indices_equals @$sSiTH : $@convention(keypath_accessor_equals) (@in_guaranteed Int, @in_guaranteed Int) -> Bool, indices_hash @$sSiTh : $@convention(keypath_accessor_hash) (@in_guaranteed Int) -> Int, external #Array.subscript<String>) (%3)
%7 = keypath $KeyPath<Trip, Optional<Int>>, (root $Trip; stored_property #Trip.note : $Optional<String>; optional_chain : $String; gettable_property $Int, id @$sSS5countSivg : $@convention(method) (@guaranteed String) -> Int, getter @$sSS5countSivpSSTK : $@convention(keypath_accessor_getter) (@in_guaranteed String) -> @out Int, external #String.count; optional_wrap : $Optional<Int>)
%9 = keypath $WritableKeyPath<Trip, String>, (root $Trip; stored_property #Trip.note : $Optional<String>; optional_force : $String)
%11 = keypath $WritableKeyPath<Spot, Double>, (root $Spot; stored_property #Spot.coords : $(lat: Double, lon: Double); tuple_element #0 : $Double)
%13 = keypath $WritableKeyPath<Spot, String>, (root $Spot; settable_property $String, id @$s7program4SpotV5labelSSvg : $@convention(method) (@guaranteed Spot) -> @owned String, getter @$s7program4SpotV5labelSSvpACTK : $@convention(keypath_accessor_getter) (@in_guaranteed Spot) -> @out String, setter @$s7program4SpotV5labelSSvpACTk : $@convention(keypath_accessor_setter) (@in_guaranteed String, @inout Spot) -> ())
%15 = keypath $ReferenceWritableKeyPath<Store, String>, (root $Store; stored_property #Store.trip : $Trip; stored_property #Trip.city : $String)Each is the root, then one entry per step, in order:
\Trip.stops[i]: stored_property #Trip.stops, then settable_property for the array's subscript — the getter and setter to call, the id that identifies the subscript, and indices [%$0 : $Int : $Int], the argument, whose value is the operand at the end of the instruction. Beside them, indices_equals and indices_hash: functions that compare and hash the argument. They are why the argument must be Hashable — the key path's own == and hash(into:) call them.
\Trip.note?.count: optional_chain : $String, the getter of count, then optional_wrap : $Optional<Int> — unwrap, take the step, wrap the result again.
\Trip.note!: optional_force : $String, an unwrap with no way out.
\Spot.coords.lat: tuple_element #0, the tuple's first element.
\Spot.label: a settable_property with its getter, its setter and its id.
\Store.trip.city: two stored_property steps, as for a struct. Its type, ReferenceWritableKeyPath, comes from the first step being a class's.
The array's subscript and String.count are also marked external: they belong to another module, the standard library. For such a step the pattern refers to a property descriptor that the other module exports, and the runtime consults it when it builds the object — so a library can change how a property is stored without breaking the key paths compiled against it.
A SIL instruction is still only a description. The object is made while the program runs, by a call into the Swift runtime — one per key path, in LLVM IR:
call ptr @swift_getKeyPath(ptr @keypath, ptr %…) #3
call ptr @swift_getKeyPath(ptr @keypath.1, ptr undef) #3
call ptr @swift_getKeyPath(ptr @keypath.3, ptr undef) #3
call ptr @swift_getKeyPath(ptr @keypath.5, ptr undef) #3
call ptr @swift_getKeyPath(ptr @keypath.7, ptr undef) #3
call ptr @swift_getKeyPath(ptr @keypath.9, ptr undef) #3Each @keypath… is a pattern: the description, stored in the program as a constant. The second argument is where the subscript differs. For it, the program passes a pointer to the argument values (ptr %…); every other path passes nothing (undef). swift_getKeyPath itself is a small shim in the runtime's C++, there so that newer compilers' patterns can be supported by older systems; the work is done by a function in the standard library, written in Swift, whose comments describe the pattern:
// Runtime entry point to instantiate a key path object.
// Note that this has a compatibility override shim in the runtime so that
// future compilers can backward-deploy support for instantiating new key path
// pattern features.
@_cdecl("swift_getKeyPathImpl")
@_unavailableInEmbedded
public func _swift_getKeyPath(pattern: UnsafeMutableRawPointer,
arguments: UnsafeRawPointer)
-> UnsafeRawPointer {
// The key path pattern is laid out like a key path object, with a few
// modifications:
// - Pointers in the instantiated object are compressed into 32-bit
// relative offsets in the pattern.
// - The pattern begins with a field that's either zero, for a pattern that
// depends on instantiation arguments, or that's a relative reference to
// a global mutable pointer variable, which can be initialized to a single
// shared instantiation of this pattern.
…
// The pattern never precomputes the capabilities of the key path (readonly/
// writable/reference-writable), nor does it encode the reference prefix.
// These are resolved dynamically, so that they always reflect the dynamic
// capability of the properties involved.
…
// If the first word is nonzero, it relative-references a cache variable
// we can use to reference a single shared instantiation of this key path.
…
// Instantiate a new key path object modeled on the pattern.
// Do a pass to determine the class of the key path we'll be instantiating
// and how much space we'll need for it.
let (keyPathClass, rootType, size, sizeWithMaxSize)
= unsafe _getKeyPathClassAndInstanceSizeFromPattern(patternPtr, arguments)
…
}A pattern without arguments comes with a cache, and the first call fills it: after that, the same pattern always gives the same object, which is why === said true for two copies of \Trip.city in one function. A pattern with arguments has a zero there instead, so each evaluation makes a new object — \Trip.stops[0] === \Trip.stops[0] was false.
The compiler writes one pattern for each different key path in what it compiles at once — and a Debug build compiles one file at a time. Write \Trip.city in two files:
struct Trip {
var city: String
}
func pathFromA() -> AnyKeyPath {
\Trip.city
}let here: AnyKeyPath = \Trip.city
print(here === pathFromA(), here == pathFromA())Built a file at a time, as a Debug build is, it prints
false trueBuilt as one module (-wmo), as a Release build is
true trueTwo patterns, two objects; one pattern, one object; equal either way. So compare key paths with ==, never ===.
The comment's last paragraph has a consequence you can watch. The pattern does not say which class to make: the compiler decides the type of a key path, but the runtime decides the class of the object, from the steps themselves. Usually the two agree. Here they do not:
struct Trip {
var city: String
private(set) var bookings = 0
mutating func book() {
bookings += 1
}
}var trip = Trip(city: "Seattle")
trip.book()
let bookings = \Trip.bookings
print(type(of: bookings))
if let writable = bookings as? WritableKeyPath<Trip, Int> {
trip[keyPath: writable] = 99
}
print(trip.bookings)It prints
WritableKeyPath<Trip, Int>
99Outside Trip.swift, private(set) forbids writing bookings, so in main.swift the compiler typed \Trip.bookings as a plain KeyPath. Write through it directly, without the cast, and it does not compile:
var trip = Trip(city: "Seattle")
let bookings = \Trip.bookings
trip[keyPath: bookings] = 99It does not compile
cannot assign through subscript: 'bookings' is a read-only key path
But here is what the compiler wrote down in main.swift:
// main
%22 = keypath $KeyPath<Trip, Int>, (root $Trip; stored_property #Trip.bookings : $Int)The type is KeyPath; the pattern says only stored_property #Trip.bookings — a stored var. From that the runtime built a WritableKeyPath, which is what type(of:) reports: it asks the object, not the compiler. So the cast succeeded, and 99 went into a property that is private(set). That is a hole in access control, not something to use — but it shows exactly where each half of a key path comes from.
The object is an instance of one of the five classes. The compiler's own description of the layout (docs/ABI/KeyPaths.md, under Further reading) begins with a header: a pointer to the Objective-C key path string — the "name" of the first section, for an @objc property, and null otherwise — and the size and flags of a buffer. The string is how Objective-C APIs can take a Swift key path:
print((\Place.name)._kvcKeyPathString as Any)
print((\Trip.city)._kvcKeyPathString as Any)
print(NSExpression(forKeyPath: \Place.name).keyPath)It prints
Optional("name")
nil
name_kvcKeyPathString reads it — public, but underscored, so not for your own code; Foundation uses it, and NSExpression(forKeyPath:) got "name" from it. The buffer holds the steps, and the standard library reads each one back as this enum:
internal enum KeyPathComponent {
…
/// The keypath projects within the storage of the outer value, like a
/// stored property in a struct.
case `struct`(offset: Int)
/// The keypath projects from the referenced pointer, like a
/// stored property in a class.
case `class`(offset: Int)
/// The keypath projects using a getter.
case get(id: ComputedPropertyID,
accessors: ComputedAccessorsPtr,
argument: ArgumentRef?)
/// The keypath projects using a getter/setter pair. The setter can mutate
/// the base value in-place.
case mutatingGetSet(id: ComputedPropertyID,
accessors: ComputedAccessorsPtr,
argument: ArgumentRef?)
/// The keypath projects using a getter/setter pair that does not mutate its
/// base.
case nonmutatingGetSet(id: ComputedPropertyID,
accessors: ComputedAccessorsPtr,
argument: ArgumentRef?)
/// The keypath optional-chains, returning nil immediately if the input is
/// nil, or else proceeding by projecting the value inside.
case optionalChain
/// The keypath optional-forces, trapping if the input is
/// nil, or else proceeding by projecting the value inside.
case optionalForce
/// The keypath wraps a value in an optional.
case optionalWrap
}A stored property of a struct is an offset into the value; of a class, an offset into the object. A computed property is its accessors and its id, plus the arguments if it is a subscript. Reading through a key path walks the steps one by one, and the code for one step is a switch on that enum:
internal func _projectReadOnly<CurValue, NewValue, LeafValue>(
_ base: CurValue,
to: NewValue.Type,
endingWith: LeafValue.Type,
_ isBreak: inout Bool,
pointer: UnsafeMutablePointer<NewValue>
) {
switch value {
case .struct(let offset):
unsafe _withUnprotectedUnsafeBytes(of: base) {
let p = unsafe $0.baseAddress._unsafelyUnwrappedUnchecked + offset
// The contents of the struct should be well-typed, so we can assume
// typed memory here.
unsafe pointer.initialize(to: p.assumingMemoryBound(to: NewValue.self).pointee)
}
case .class(let offset):
…
let baseObj: AnyObject = Builtin.reinterpretCast(base)
let basePtr = UnsafeRawPointer(Builtin.bridgeToRawPointer(baseObj))
…
let offsetAddress = unsafe basePtr.advanced(by: offset)
…
unsafe pointer.initialize(
to: offsetAddress.assumingMemoryBound(to: NewValue.self).pointee
)
case .get(id: _, accessors: let accessors, argument: let argument),
.mutatingGetSet(id: _, accessors: let accessors, argument: let argument),
.nonmutatingGetSet(id: _, accessors: let accessors, argument: let argument):
let getter: ComputedAccessorsPtr.Getter<CurValue, NewValue> = unsafe accessors.getter()
unsafe pointer.initialize(
to: getter(
base,
argument?.data.baseAddress ?? accessors._value,
argument?.data.count ?? 0
)
)
case .optionalChain:
…
// We found nil.
isBreak = true
…
}A struct step is arithmetic: the address of the value, plus the offset. A class step takes the object's address and adds the offset. A computed step calls the getter and hands it the arguments. An optional chain that meets nil sets isBreak, and the walk stops there. == is a walk too. It first asks whether the two are the same object, then whether they have the same class, and then compares the two buffers step by step — offsets against offsets, computed steps by their id and then their arguments, through the indices_equals function the compiler stored:
public static func ==(a: AnyKeyPath, b: AnyKeyPath) -> Bool {
// Fast-path identical objects
if a === b {
return true
}
// Short-circuit differently-typed key paths
if type(of: a) != type(of: b) {
return false
}
…
internal static func ==(a: KeyPathComponent, b: KeyPathComponent) -> Bool {
switch (a, b) {
case (.struct(offset: let a), .struct(offset: let b)),
(.class (offset: let a), .class (offset: let b)):
return a == b
case (.optionalChain, .optionalChain),
(.optionalForce, .optionalForce),
(.optionalWrap, .optionalWrap):
return true
case (.get(id: let id1, accessors: _, argument: let argument1),
.get(id: let id2, accessors: _, argument: let argument2)),
…
if id1 != id2 {
return false
}
if let arg1 = unsafe argument1, let arg2 = unsafe argument2 {
return unsafe arg1.witnesses.equals(
arg1.data.baseAddress.unsafelyUnwrapped,
arg2.data.baseAddress.unsafelyUnwrapped,
arg1.data.count - arg1.witnessSizeAdjustment)
}
…A path made only of struct steps comes down to a single offset, and the standard library uses that twice. Reading through such a path skips the walk altogether:
internal final func _projectReadOnly(from root: Root) -> Value {
let (rootType, valueType) = Self._rootAndValueType
// One performance improvement is to skip right to Value
// if this keypath traverses through structs only.
if let offset = getOffsetFromStorage() {
return unsafe _withUnprotectedUnsafeBytes(of: root) {
let pointer = unsafe $0.baseAddress._unsafelyUnwrappedUnchecked + offset
return unsafe pointer.assumingMemoryBound(to: Value.self).pointee
}
}
…And MemoryLayout.offset(of:) (SE-0210, Swift 4.2) hands the offset to you:
final class Pin {
var spot = Spot(name: "Pier", coords: (lat: 47.6, lon: -122.3))
}print(MemoryLayout<Spot>.offset(of: \Spot.coords.lon) as Any)
print(MemoryLayout<Spot>.offset(of: \Spot.self) as Any)
print(MemoryLayout<Spot>.offset(of: \Spot.label) as Any)
print(MemoryLayout<Pin>.offset(of: \Pin.spot) as Any)It prints
Optional(24)
Optional(0)
nil
nil @usableFromInline // Exposed as public API by MemoryLayout<Root>.offset(of:)
internal var _storedInlineOffset: Int? {
return unsafe withBuffer {
var buffer = unsafe $0
// The identity key path is effectively a stored keypath of type Self
// at offset zero
if unsafe buffer.data.isEmpty { return 0 }
var offset = 0
while true {
let (rawComponent, optNextType) = unsafe buffer.next()
switch rawComponent.header.kind {
case .struct:
offset += rawComponent._structOrClassOffset
case .class, .computed, .optionalChain, .optionalForce, .optionalWrap, .external:
return .none
}
if optNextType == nil { return .some(offset) }
}
fatalError()
}
}Struct steps add up: name, a String, takes 16 bytes and lat 8, so coords.lon is at 24. The identity path is at 0. A computed property is not stored at all, and a class's property is not inside the value — a Pin value is only a reference — so for those the answer is nil.
All of this — the enum, the buffer, the walk — is the runtime's own business. The same document says that only the patterns are a fixed part of Swift's ABI, because compilers write them into programs; the runtime may lay out the objects differently in a later version, as long as it can still build them from the patterns of earlier compilers.
A key path holds three things: where it starts, what it gives you, and which property it reads. Its type holds only the first two. \Trip.id and \Trip.nights both lead from a Trip to an Int, so with nothing but those two types a program cannot tell which Int is meant. Reflection can list every Int in a trip, and it finds two:
// Every value of one type inside a value, found by reflection: all a program could do
// with nothing but the two types a key path's type names.
func candidates<Root, Value>(in root: Root, of _: Value.Type) -> [Value] {
Mirror(reflecting: root).children.compactMap { child in
child.value as? Value
}
}let trip = Trip(id: 7, city: "Seattle", nights: 5)
print(candidates(in: trip, of: Int.self))
print(trip[keyPath: \Trip.nights], MemoryLayout<Trip>.offset(of: \Trip.nights)!)It prints
[7, 5]
5 24The third thing is in the object. Read the first step each object holds, as a number:
// The first step a key path object holds, as a number. On iOS 27 it is the 32-bit word
// 32 bytes in: after the object's header, the string pointer and the size of the buffer.
func firstStep(_ path: AnyKeyPath) -> String {
let object = UnsafeRawPointer(Unmanaged.passUnretained(path).toOpaque())
let step = object.load(fromByteOffset: 32, as: UInt32.self)
let text = "0x" + String(step, radix: 16)
if step & 0x7F_FFFF == 0x7F_FFFF {
return text + " then " + String(object.load(fromByteOffset: 36, as: UInt32.self))
}
return text
}let paths: [KeyPath<Trip, Int>] = [\.id, \.nights]
for path in paths {
print(path, firstStep(path))
}It prints
\Trip.id 0x1000000
\Trip.nights 0x1800018In 0x1800018 the top byte, 01, means a stored property of a struct; the 8 after it, that the property can be written; the rest, 0x18, is the offset: 24. \Trip.id holds 0x1000000: stored, read-only, at 0. That offset is the whole answer to "which Int". The compiler worked it out from Trip's layout and wrote it into the pattern, and a read is the trip's address plus 24. The property's name plays no part while the program runs.
So what does a key path hold when the compiler cannot know the offset? Something that leads to it — one of three things.
In none of these is a key path only its two types. The compiler always knows which property — it found the declaration when it read the name — and what it can lack is where the property is. Then it writes down something that leads there.
In generic code, the property's entry in the type's metadata. A generic function is compiled once, for every type it will ever be called with, and each call hands it the type as a hidden argument. Here are two Int properties whose offsets depend on First, and two paths of one type, KeyPath<Trio<First>, Int>:
struct Trio<First> {
let first: First
let second: Int
let third: Int
}
// Compiled once, for every First. Both paths are KeyPath<Trio<First>, Int>.
func secondPath<First>(_: First.Type) -> AnyKeyPath {
\Trio<First>.second
}
func thirdPath<First>(_: First.Type) -> AnyKeyPath {
\Trio<First>.third
}// secondPath<A>(_:)
%2 = keypath $KeyPath<Trio<First>, Int>, <τ_0_0> (root $Trio<τ_0_0>; stored_property #Trio.second : $Int) <First>
// thirdPath<A>(_:)
%2 = keypath $KeyPath<Trio<First>, Int>, <τ_0_0> (root $Trio<τ_0_0>; stored_property #Trio.third : $Int) <First>The patterns are written for any First — τ_0_0 — so neither can hold an offset. Each still names its property: stored_property #Trio.second, stored_property #Trio.third. In the program, that name becomes a place to look: the property's own entry in Trio's metadata, the runtime's record of the type. The runtime fills those entries in the first time each Trio<First> is used, laying the struct out from First's size, and swift_getKeyPath copies the right one into the object. Read both:
// Trio's field offsets, read from its metadata: the type's descriptor says where they start.
func fieldOffsets<First>(of _: Trio<First>.Type) -> [UInt32] {
let metadata = unsafeBitCast(Trio<First>.self, to: UnsafeRawPointer.self)
let descriptor = metadata.load(fromByteOffset: 8, as: UnsafeRawPointer.self)
let start = Int(descriptor.load(fromByteOffset: 24, as: UInt32.self)) * 8
return (0..<3).map { field in
metadata.load(fromByteOffset: start + 4 * field, as: UInt32.self)
}
}func show<First>(_: First.Type) {
print("Trio<\(First.self)>:", firstStep(secondPath(First.self)), "|", firstStep(thirdPath(First.self)),
"|", fieldOffsets(of: Trio<First>.self))
}
show(Int8.self)
show(String.self)It prints
Trio<Int8>: 0x17fffff then 8 | 0x17fffff then 16 | [0, 8, 16]
Trio<String>: 0x17fffff then 16 | 0x17fffff then 24 | [0, 16, 24]0x17fffff means "the offset is in the next word", and each next word matches its own entry in the metadata. After an Int8, second is at 8, not 1, because an Int must start on a multiple of 8; after a 16-byte String, at 16. third follows 8 bytes later either way.
For a type from another library, that library's own description. Foundation may change how DateInterval is laid out from one OS version to the next, so code compiled against it cannot assume that start is stored, let alone where:
let start = \DateInterval.start
print(type(of: start), firstStep(start), MemoryLayout<DateInterval>.offset(of: start) as Any)// static Program.main()
%2 = keypath $any WritableKeyPath<DateInterval, Date> & Sendable, (root $DateInterval; settable_property $Date, id @$s10Foundation12DateIntervalV5startAA0B0Vvg : $@convention(method) (@in_guaranteed DateInterval) -> @out Date, getter @$s10Foundation12DateIntervalV5startAA0B0VvpACTK : $@convention(keypath_accessor_getter) (@in_guaranteed DateInterval) -> @out Date, setter @$s10Foundation12DateIntervalV5startAA0B0VvpACTk : $@convention(keypath_accessor_setter) (@in_guaranteed Date, @inout DateInterval) -> (), external #DateInterval.start)Your code holds a getter and a setter, and external #DateInterval.start: a reference to the property descriptor Foundation exports for start (Inside, part 1). When the object is made, the runtime asks that descriptor, and Foundation's answer takes the place of your getter and setter:
It prints
WritableKeyPath<DateInterval, Date> 0x1800000 Optional(0)A stored property at byte 0, settable — learned while the program ran, from Foundation's own description.
For a computed property, its getter. There is no offset at all. The object holds the getter, and the setter if there is one, and the getter's address doubles as the property's id — the gettable_property of Inside, part 1.
| The property | Your compiled code holds | The object holds |
|---|---|---|
| stored, in a layout the compiler knows | its offset | its offset |
| stored, in generic code | its entry in the type's metadata | its offset, copied from that entry |
| of another library's type | that library's property descriptor, a getter and a setter | what the descriptor says — here, an offset |
| computed | its getter and setter | the same; the getter's address is its id |
These numbers are the runtime's own business, as Inside, part 2 said: only the patterns are a fixed part of Swift's ABI. firstStep reads the object as iOS 27 lays it out — fine for looking, wrong for anything you ship.
[keyPath:] compiles to[keyPath:] is not declared anywhere in Swift. The compiler knows it, and turns each use into a call to one of a few functions in the standard library marked as compiler intrinsics:
@_silgen_name("swift_getAtKeyPath")
public // COMPILER_INTRINSIC
func _getAtKeyPath<Root, Value>(
root: Root,
keyPath: KeyPath<Root, Value>
) -> Value {
return keyPath._projectReadOnly(from: root)
}
// SILGen emits calls to `swift_readAtKeyPath`, `swift_modifyAtWritableKeyPath`
// and `swift_modifyAtReferenceWritableKeyPath` for key path subscript accesses
// (see `getKeyPathProjectionCoroutine` in lib/SILGen/SILGen.cpp).
…
@_silgen_name("swift_setAtWritableKeyPath")
public // COMPILER_INTRINSIC
func _setAtWritableKeyPath<Root, Value>(
root: inout Root,
keyPath: WritableKeyPath<Root, Value>,
value: __owned Value
) {
if type(of: keyPath).kind == .reference {
return unsafe _setAtReferenceWritableKeyPath(root: root,
keyPath: _unsafeUncheckedDowncast(keyPath,
to: ReferenceWritableKeyPath<Root, Value>.self),
value: value)
}
// TODO: we should be able to do this more efficiently than projecting.
let (addr, owner) = unsafe _withUnprotectedUnsafePointer(to: &root) {
unsafe keyPath._projectMutableAddress(from: $0)
}
unsafe addr.pointee = value
…
}Which one it calls depends on the access. In code that receives the key path as a value, a call is all there can be:
func read<Root, Value>(_ path: KeyPath<Root, Value>, from root: Root) -> Value {
root[keyPath: path]
}
func write<Root, Value>(_ value: Value, to path: WritableKeyPath<Root, Value>, in root: inout Root) {
root[keyPath: path] = value
}
func bump<Root>(_ path: WritableKeyPath<Root, Int>, in root: inout Root) {
root[keyPath: path] += 1
}var trip = Trip(id: 1, city: "Seattle", nights: 5)
write("Victoria", to: \.city, in: &trip)
bump(\.nights, in: &trip)
print(read(\.city, from: trip), read(\.nights, from: trip))It prints
Victoria 6// read<A, B>(_:from:)
%7 = function_ref @swift_getAtKeyPath : $@convention(thin) <τ_0_0, τ_0_1> (@in_guaranteed τ_0_0, @guaranteed KeyPath<τ_0_0, τ_0_1>) -> @out τ_0_1
%8 = apply %7<Root, Value>(%0, %2, %1) : $@convention(thin) <τ_0_0, τ_0_1> (@in_guaranteed τ_0_0, @guaranteed KeyPath<τ_0_0, τ_0_1>) -> @out τ_0_1
// write<A, B>(_:to:in:)
%10 = function_ref @swift_setAtWritableKeyPath : $@convention(thin) <τ_0_0, τ_0_1> (@inout τ_0_0, @guaranteed WritableKeyPath<τ_0_0, τ_0_1>, @in τ_0_1) -> ()
%11 = apply %10<Root, Value>(%9, %1, %7) : $@convention(thin) <τ_0_0, τ_0_1> (@inout τ_0_0, @guaranteed WritableKeyPath<τ_0_0, τ_0_1>, @in τ_0_1) -> ()
// bump<A>(_:in:)
%7 = function_ref @swift_modifyAtWritableKeyPath : $@yield_once @convention(thin) <τ_0_0, τ_0_1> (@inout τ_0_0, @guaranteed WritableKeyPath<τ_0_0, τ_0_1>) -> @yields @inout τ_0_1
(%8, %9) = begin_apply %7<Root, Int>(%6, %0) : $@yield_once @convention(thin) <τ_0_0, τ_0_1> (@inout τ_0_0, @guaranteed WritableKeyPath<τ_0_0, τ_0_1>) -> @yields @inout τ_0_1A read is swift_getAtKeyPath. An assignment is swift_setAtWritableKeyPath — which, as quoted above, first asks whether the object is really a ReferenceWritableKeyPath, and hands it on if so. += reads and writes in one access, so it is swift_modifyAtWritableKeyPath, a coroutine: begin_apply starts it and gets back the address of the value, the code adds 1 there, and end_apply finishes the access.
When the compiler can see which key path it is — a literal, in the same function — it does better, even in a Debug build:
var trip = Trip(id: 1, city: "Seattle", nights: 5)
trip[keyPath: \Trip.city] = "Victoria"
print(trip[keyPath: \Trip.city])
trip[keyPath: \Trip.nights] += 1
print(trip.nights)It prints
Victoria
6// static Program.main()
%30 = struct_element_addr %27, #Trip.city
%47 = struct_element_addr %44, #Trip.city
%61 = keypath $WritableKeyPath<Trip, Int>, (root $Trip; stored_property #Trip.nights : $Int)
%64 = function_ref @swift_modifyAtWritableKeyPath : $@yield_once @convention(thin) <τ_0_0, τ_0_1> (@inout τ_0_0, @guaranteed WritableKeyPath<τ_0_0, τ_0_1>) -> @yields @inout τ_0_1
(%65, %66) = begin_apply %64<Trip, Int>(%63, %61) : $@yield_once @convention(thin) <τ_0_0, τ_0_1> (@inout τ_0_0, @guaranteed WritableKeyPath<τ_0_0, τ_0_1>) -> @yields @inout τ_0_1The write and the read of city each became struct_element_addr … #Trip.city, the instruction that trip.city itself becomes — and the key path for city is gone altogether: no keypath makes it, and no swift_getAtKeyPath or swift_setAtWritableKeyPath is called (this page's proof fails if either is left in main). Only += is still a key path: \Trip.nights is made, and handed to the runtime's coroutine.
That is the optimizer's work. A Debug build runs a few of its passes, and one of them, onone-simplification, simplifies every instruction whose type declares that it can be simplified even then:
let ononeSimplificationPass = FunctionPass(name: "onone-simplification") {
(function: Function, context: FunctionPassContext) in
runSimplification(on: function, context, preserveDebugInfo: true) {
if let i = $0 as? OnoneSimplifiable {
i.simplify($1)
}
}
}A call — ApplyInst, in the optimizer — is one of them:
extension ApplyInst : OnoneSimplifiable, SILCombineSimplifiable {
func simplify(_ context: SimplifyContext) {
if tryTransformThickToThinCallee(of: self, context) {
return
}
if context.tryOptimizeKeypath(apply: self) {
for arg in arguments {
if let keypath = arg.getRootKeypath() {
context.notifyInstructionChanged(keypath)
}
}
context.erase(instruction: self)
return
}
…
}
}OnoneSimplifiable is what puts it in a Debug build. tryOptimizeKeypath is written in C++, and the function it calls draws the rewrite in its comment:
/// Try to optimize a keypath application with an apply instruction.
///
/// Replaces (simplified SIL):
/// %kp = keypath ...
/// apply %keypath_runtime_function(%addr, %kp, %root_object)
/// with:
/// %addr = struct_element_addr/ref_element_addr %root_object
/// ...
/// load/store %addr
bool swift::tryOptimizeKeypathApplication(ApplyInst *AI,
SILFunction *callee, SILBuilder Builder) {
…
if (callee->getName() == "swift_setAtWritableKeyPath" ||
callee->getName() == "swift_setAtReferenceWritableKeyPath") {
…
} else if (callee->getName() == "swift_getAtKeyPath") {
…
} else {
return false;
}
…
}
…
bool SILCombiner::tryOptimizeInoutKeypath(BeginApplyInst *AI) {
…
if (callee->getName() == "swift_modifyAtWritableKeyPath" ||
callee->getName() == "swift_modifyAtReferenceWritableKeyPath") {
…It knows the two setters and the getter. The coroutine behind += is handled by tryOptimizeInoutKeypath, a method of SILCombine, a pass that runs only when the optimizer runs in full — a Release build. Built with -O:
@inline(never) // keeps the function whole, so that its SIL can be read
func extend(_ trip: inout Trip) {
trip[keyPath: \Trip.nights] += 1
}// extend(_:)
%3 = struct_element_addr %0, #Trip.nights
%7 = builtin "sadd_with_overflow_Int64"(%5, %2, %6) : $(Builtin.Int64, Builtin.Int1)No keypath, no runtime call — this proof, too, fails if either is there. One struct_element_addr and the addition. A key path the compiler can see costs what the property costs; one it cannot see is a call into the runtime and a walk through the steps.
@dynamicMemberLookup, in fullThe attribute changes one rule. When x.name names a member that the type of x does not have, the compiler does not report an error: it rewrites the expression as a call to a subscript called subscript(dynamicMember:), and passes the name. What exactly it passes depends on what the subscript takes, and that came in two steps.
First, a name (SE-0195, Swift 4.2). The subscript takes a String, and every name is accepted:
@dynamicMemberLookup
struct Settings {
var values = ["theme": "dark"]
subscript(dynamicMember name: String) -> String? {
values[name]
}
}let settings = Settings()
print(settings.theme as Any)
print(settings.theem as Any)It prints
Optional("dark")
nilsettings.theem compiled, because to a String subscript every name is as good as another, and it became settings[dynamicMember: "theem"]. That version was made for calling code written in dynamic languages, such as Python, where names really are only known while the program runs. In Swift code it gives up what the compiler would have checked.
Then, a key path (SE-0252, Swift 5.1). The subscript takes a key path from some type; the compiler accepts a name only if that type has such a member, and passes the member's key path. Lesson 13's Ref takes a WritableKeyPath, so its dynamic members are the properties it can write. A type may have more than one such subscript. Give Ref a second, for paths that can only read:
extension Ref {
/// A member that cannot be written: its value, read through the key path.
subscript<Field>(dynamicMember path: KeyPath<Value, Field>) -> Field {
value[keyPath: path]
}
}var trip = Trip(id: 1, city: "Seattle", nights: 5)
let tripRef = Ref(get: { trip }, set: { trip = $0 })
print(type(of: tripRef.city))
print(type(of: tripRef.id))
print(type(of: tripRef.city.count))It prints
Ref<String>
Int
Intcity fits both subscripts, and the compiler prefers the one whose parameter is more specific — WritableKeyPath is a subclass of KeyPath — as it does for any overloaded call: a Ref<String>. id is a let, so only the reading subscript fits: an Int. tripRef.city.count is a second lookup, on that Ref<String>. count has no setter, so again an Int. SwiftUI's Binding has only the writable subscript, which is why $fair.judge.count does not compile (lesson 13's cards).
The rewrite is a last resort: a member the type really has always wins.
struct Reading {
var value: Int
}var reading = Reading(value: 7)
let ref = Ref(get: { reading }, set: { reading = $0 })
print(type(of: ref.value))
print(type(of: ref[dynamicMember: \.value]))It prints
Reading
Ref<Int>Ref has a value of its own, so ref.value is that — the whole Reading — and no lookup happens. To reach Reading.value, call the subscript yourself, with the key path.
The iOS 27 SDK's SwiftUI and SwiftUICore interfaces take key paths in the shapes below. The kind each asks for follows one rule: KeyPath where SwiftUI only reads, WritableKeyPath where it writes into a value it keeps, ReferenceWritableKeyPath where the value lives in an object.
| You write | SwiftUI takes | What for |
|---|---|---|
$trip.city | Binding's subscript(dynamicMember:): a WritableKeyPath<Value, Subject> | a binding to one field — it must be able to write |
$store.lastOpened | a ReferenceWritableKeyPath, in Bindable's subscript and in the $ of @ObservedObject and @EnvironmentObject | a binding to a property of an object |
@Environment(\.colorScheme) | KeyPath<EnvironmentValues, Value> | reads one value of the environment |
.environment(\.locale, …), .transformEnvironment(\.font) { … } | WritableKeyPath<EnvironmentValues, V> | writes one, for the views inside |
ForEach(trips, id: \.city), List(trips, id: \.city) | KeyPath<Data.Element, ID> | reads each element's identity — see ForEach, in full |
TableColumn("City", value: \.city) | KeyPath<RowValue, …> | reads what a column shows, and sorts by |
@FocusedValue(\.trip); .focusedValue(\.trip, trip) | KeyPath<FocusedValues, Value?>; WritableKeyPath<FocusedValues, Value?> | reads, and publishes, a value of the view that has focus |
KeyframeTrack(\.scale) { … } | WritableKeyPath<Root, Value> | the property a keyframe animation writes, frame by frame |
.containerValue(\.x, …), withTransaction(\.x, …) { … } | WritableKeyPath<ContainerValues, V>, WritableKeyPath<Transaction, V> | writes a value that a container, or a transaction, carries |
The environment is the clearest case, because its code is published in the SDK. The property wrapper keeps the key path in an enum; the modifier keeps a writable one in a struct:
@frozen @propertyWrapper public struct Environment<Value> : DynamicProperty {
@usableFromInline
@frozen internal enum Content : @unchecked Sendable {
case keyPath(KeyPath<EnvironmentValues, Value>)
case value(Value)
}
@usableFromInline
internal var content: Environment<Value>.Content
@inlinable public init(_ keyPath: KeyPath<EnvironmentValues, Value>) {
content = .keyPath(keyPath)
}
@inlinable public var wrappedValue: Value {
get {
switch content {
case let .value(value):
return value
case let .keyPath(keyPath):
…
return EnvironmentValues()[keyPath: keyPath]
}
}
}
…
}
extension View {
@inlinable nonisolated public func environment<V>(_ keyPath: WritableKeyPath<EnvironmentValues, V>, _ value: V) -> some View {
modifier(_EnvironmentKeyWritingModifier(keyPath: keyPath, value: value))
}
}
@frozen nonisolated public struct _EnvironmentKeyWritingModifier<Value> : ViewModifier, _GraphInputsModifier {
public var keyPath: WritableKeyPath<EnvironmentValues, Value>
public var value: Value
…
}Outside a view, wrappedValue falls back to EnvironmentValues()[keyPath: keyPath] (after logging that it is being read outside a view): the default value, read through the key path. Installed in a view, the property holds .value instead, which SwiftUI fills in from the environment where the view is — in code that is not published.
EnvironmentValues is a struct like any other, with a public initializer, so its key paths can be tried on their own:
var values = EnvironmentValues()
print(values.colorScheme)
values[keyPath: \.colorScheme] = .dark
print(values.colorScheme)
print(type(of: \EnvironmentValues.colorScheme))
print(type(of: \EnvironmentValues.dismiss))It prints
light
dark
WritableKeyPath<EnvironmentValues, ColorScheme>
KeyPath<EnvironmentValues, DismissAction>colorScheme has a setter. dismiss has only a getter: a view may call the action it was given, but not replace it for the views below. So this cannot be written:
import SwiftUI
struct Sheet: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
Text("Done")
.environment(\.dismiss, dismiss)
}
}It does not compile
cannot convert value of type 'any KeyPath<EnvironmentValues, DismissAction> & Sendable' to expected argument type 'WritableKeyPath<EnvironmentValues, DismissAction>'
(any … & Sendable is Swift 6 noting that the literal captures nothing, so it is safe to share between threads — SE-0418.)
A key path leads only through properties and subscripts that can be read the plain way: right away, without changing anything, and without failing. The compiler says so, case by case:
key path cannot refer to instance method 'uppercased()'async or throws: cannot form key path to property with 'throws' or 'async'lazy property, whose getter changes the struct the first time it runs: key path cannot refer to 'summary', which has a mutating getterHashable: subscript index of type 'Place' in a key path must be Hashableno exact matches in call to instance method 'map'Binding, a property with no setter ($fair.judge.count): cannot assign to property: 'count' is a get-only property.environment(_:_:), a value with no setter: cannot convert value of type 'any KeyPath<EnvironmentValues, DismissAction> & Sendable' to expected argument type 'WritableKeyPath<EnvironmentValues, DismissAction>'struct Forecast {
var today: String {
get async {
"sun"
}
}
}struct Report {
lazy var summary = "made once, when first read"
}Methods are the one most often asked for. A proposal to allow them, \String.uppercased() included (SE-0479), was returned for revision; in Swift 6.4 a method is still out of reach. Wrap it in a computed property, or use a closure.
20 cards, for this guide only: they are not in lesson 13's deck. Answer in your head first, then tap the card.
A string, for Objective-C. #keyPath(Place.name) (Swift 3) checks the name as it compiles, and is then just the name — it printed name String. Reading through it is key-value coding: the name is looked up as the program runs and the type is lost, so value(forKeyPath:) returns Any? — Optional(Seattle). Only @objc properties of NSObject subclasses take part.
KeyPath, a WritableKeyPath or a ReferenceWritableKeyPath?The declarations on its route. A let, or a getter without a setter, anywhere on it: KeyPath, which reads. Every step settable, and writing changes the root: WritableKeyPath. Settable, but writing leaves the root itself unchanged — a class's property, anything reached through a reference, a nonmutating set: ReferenceWritableKeyPath. \Trip.id printed KeyPath<Trip, Int>; \Trip.city, WritableKeyPath<Trip, String>.
Holder is a struct, and holder a let. Holder.store holds an object. Can you write through \Holder.store.trip.city?Yes. It printed Victoria. The path passes through a reference, so the write lands inside the object, and holder itself does not change. That makes it a ReferenceWritableKeyPath, the kind that writes through a constant.
bookings is private(set), and main.swift is outside its file. What does type(of: \Trip.bookings) print there?WritableKeyPath<Trip, Int> — though the compiler typed it KeyPath, and writing through it directly does not compile there. type(of:) asks the object, and the runtime made the object from the pattern, which says only "stored var". So the cast succeeded, and it printed 99: a hole in private(set), not a feature.
\Trip.stops[1], \Trip.note?.count, \Trip.note!, \[String: Int].["Seattle"], \Spot.coords.lat (or .0). An optional chain makes the path read-only and its value optional; ! keeps it writable. It printed Olympia, then ["Portland", "Olympia"], then Optional(11)…
Atlas has a subscript that takes a Place, a plain struct. Can a key path go through it?No — not unless Place is Hashable. subscript index of type 'Place' in a key path must be Hashable The argument becomes part of the key path, and two key paths are compared and hashed step by step, arguments included. Add : Hashable to Place and it compiles.
\Trip.stops and \[String].count into one key path. What kind is it?stops.appending(path: \[String].count) — a KeyPath<Trip, Int>. A joined path can write only if both halves can, and count cannot. With \[String].[0] instead: WritableKeyPath<Trip, String>.
trips.map(\.city)?A closure: implicit closure #1 captures the key path and reads through it with swift_getAtKeyPath — trips.map { $0[keyPath: \Trip.city] }, written for you (SE-0249, Swift 5.2). It printed ["Seattle", "Victoria"].
let city = \Trip.city. Does trips.map(city) compile?No. no exact matches in call to instance method 'map' Only a key path literal is turned into a closure. A key path in a variable is an object, not a function: write trips.map { $0[keyPath: city] }.
\Trip.stops[0] == \Trip.stops[0] is true. Is \Trip.stops[0] === \Trip.stops[0]?No. A key path with a subscript is made afresh each time the expression runs, so the two are equal but not the same object. \Trip.city, with no arguments, is made once and shared — within one file, at least: === is true. And a path made with appending equals the literal, but is a new object.
MemoryLayout<Spot>.offset(of:) takes a key path. Which of \Spot.coords.lon, \Spot.self, \Spot.label (computed) and \Pin.spot (a class's) have an offset?The first two. Optional(24) and Optional(0): struct steps add up to a place inside the value, and the identity path is at 0. A computed property is stored nowhere, and a class's property is not inside the value — the value is a reference — so both give nil.
+= through a key path compile to?Calls into the standard library: swift_getAtKeyPath, swift_setAtWritableKeyPath, and swift_modifyAtWritableKeyPath — a coroutine that hands back the value's address for the +=. It printed Victoria 6.
Settings is @dynamicMemberLookup, and its subscript takes a String. What does the typo settings.theem do?It compiles, runs, and gives nil. It printed Optional("dark"), then nil. To a String subscript every name is acceptable: settings.theem becomes settings[dynamicMember: "theem"], and nothing checks it. A key-path subscript accepts only members that exist.
Ref has a dynamic-member subscript for WritableKeyPaths. Add one for read-only members, so that tripRef.id works.A second subscript(dynamicMember:), taking a KeyPath and returning the value. Where both fit, the compiler prefers WritableKeyPath, the more specific type: tripRef.city is still a Ref<String>. tripRef.id is an Int, and so is tripRef.city.count.
Reading has a property called value — and so does Ref. If ref is a Ref<Reading>, what is ref.value?Ref's own value: a Reading. The rewrite is a last resort, used only when the type has no member of that name. To reach Reading.value, call the subscript yourself — ref[dynamicMember: \.value] is a Ref<Int>.
@Environment(\.colorScheme) takes a key path. Into what — and can you use it outside a view?Into EnvironmentValues, a struct with a public initializer: light, then dark after a write through the key path. @Environment keeps the key path; .environment(_:_:) takes a WritableKeyPath, so only values with a setter can be passed down.
dismiss action down with .environment(\.dismiss, dismiss)?No — it does not compile. cannot convert value of type 'any KeyPath<EnvironmentValues, DismissAction> & Sendable' to expected argument type 'WritableKeyPath<EnvironmentValues, DismissAction>' dismiss has only a getter, so \.dismiss is a KeyPath, and .environment(_:_:) writes: it takes a WritableKeyPath.
\Trip.id and \Trip.nights both lead from a Trip to an Int. With nothing but those two types, could a program find the value a path reads?No. Reflection finds every Int in the trip, [7, 5], and nothing in the two types picks one. The third thing a key path holds does: which property, and for a stored property that is its offset. \Trip.nights is at 24, so it printed 5 24.
\Trip.nights hold that its type, WritableKeyPath<Trip, Int>, does not?Which property. Its first step is 0x1800018: a stored property of a struct (01), settable (8), at byte 24 (0x18) — an offset the compiler worked out from Trip's layout. \Trip.id, the same type here, holds 0x1000000: read-only, at 0.
\Trio<First>.second and \Trio<First>.third are both KeyPath<Trio<First>, Int>, and neither offset is known when compiling. How does each know which Int it reads?Its pattern names the property — stored_property #Trio.second or #Trio.third — and in the program that becomes the property's own entry in Trio's metadata. The runtime fills the entries in when it lays the type out, and each object copies its own: it printed Trio<Int8>: 0x17fffff then 8 | 0x17fffff then 16 | [0, 8, 16], then Trio<String>: 0x17fffff then 16 | 0x17fffff then 24 | [0, 16, 24]. The compiler always knows which property; here it only lacks where.