← All lessonsGuide · Key paths, in full
Guide

Key paths, in full

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.

1 · from lesson 13

From a string to a value

Before 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:

Place, an Objective-C class
import Foundation

final class Place: NSObject {
    @objc var name = "Seattle"
}
A key path, the 2016 way
let path = #keyPath(Place.name)
print(path, type(of: path))
print(Place().value(forKeyPath: path) as Any)

It prints

Console
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:

Trip
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.

2 · from lesson 13

Five classes, and what decides yours

From the iOS 27 SDK's Swift interface (shortened) — not code to type
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:

An AnyKeyPath
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

Console
Optional("Seattle")
nil
Trip String

A 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:

Every sort of property
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
}
The kind of a path through each
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

Console
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 pathIts kindBecause
\Trip.idKeyPathid is a let
\Trip.cityWritableKeyPatha var: writing changes the struct, so the struct must be in a var
\Trip.shoutKeyPatha computed property with no setter
\Trip.nicknameWritableKeyPatha computed property with a setter. A struct's setter is mutating: it changes the struct
\Trip.labelReferenceWritableKeyPatha nonmutating set changes nothing inside the struct, so the struct may be a constant
\Store.nameKeyPatha let, in a class
\Store.trip.cityReferenceWritableKeyPaththe first step is a property of an object; the write lands inside the object
\Holder.store.trip.cityReferenceWritableKeyPathHolder is a struct, but store holds a reference: the write lands in the object, and the Holder does not change
\Trip.Type.madeReferenceWritableKeyPath from Trip.Typea 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:

Writing through a let
let holder = Holder(store: Store())
holder[keyPath: \Holder.store.trip.city] = "Victoria"
print(holder.store.trip.city)

It prints

Console
Victoria

There 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.

3 · from lesson 13

Every step a path can take

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:

Spot, a struct with a tuple and a computed property
struct Spot {
    var name: String
    var coords: (lat: Double, lon: Double)

    var label: String {
        get {
            name.uppercased()
        }
        set {
            name = newValue
        }
    }
}
One step of each kind
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

Console
Olympia
["Portland", "Olympia"]
Optional(11)
aisle
Optional(5)
48.4 -122.3
And their kinds
print(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

Console
WritableKeyPath<Trip, String>
KeyPath<Trip, Optional<Int>>
WritableKeyPath<Trip, String>
WritableKeyPath<Trip, Trip>
WritableKeyPath<Dictionary<String, Int>, Optional<Int>>
WritableKeyPath<Spot, Double>
StepWrittenWhat 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?.countThe 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.0By label or by position; writable, like a stored property
Nothing\Trip.selfThe 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:

Atlas, whose subscript takes a Place
struct Place {
    var name: String
}

struct Atlas {
    subscript(place: Place) -> String {
        place.name.uppercased()
    }
}
A key path through it
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:

Joining two paths
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

Console
KeyPath<Trip, Int>
WritableKeyPath<Trip, String>
1 Tacoma

The joined path can write only if both halves can. \[String].count cannot, so count is a KeyPath; \[String].[0] can, so first is writable.

4 · from lesson 13

A key path where a function goes

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:

map with a key path
let trips = [Trip(id: 1, city: "Seattle", nights: 5), Trip(id: 2, city: "Victoria", nights: 1)]
print(trips.map(\.city))

It prints

Console
["Seattle", "Victoria"]
In SIL: main, and the closure the compiler wrote
// 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_1

main 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:

A key path from a variable
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] }.

5 · from lesson 13

Equal, hashable — and the same object

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:

Comparing paths
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

Console
true false
true
true false
false
Nights 5
City Seattle

The 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.

6 · from lesson 13

Inside, part 1: the pattern the compiler writes

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:

Store, a class
final class Store {
    var trip = Trip(id: 1, city: "Seattle", nights: 5)
}
Six key paths
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)
In SIL
// 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:

  1. \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.

  2. \Trip.note?.count: optional_chain : $String, the getter of count, then optional_wrap : $Optional<Int> — unwrap, take the step, wrap the result again.

  3. \Trip.note!: optional_force : $String, an unwrap with no way out.

  4. \Spot.coords.lat: tuple_element #0, the tuple's first element.

  5. \Spot.label: a settable_property with its getter, its setter and its id.

  6. \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:

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) #3

Each @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:

From the Swift standard library's source, stdlib/public/core/KeyPath.swift (shortened) — not code to type
// 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:

A.swift
struct Trip {
    var city: String
}

func pathFromA() -> AnyKeyPath {
    \Trip.city
}
main.swift
let here: AnyKeyPath = \Trip.city
print(here === pathFromA(), here == pathFromA())

Built a file at a time, as a Debug build is, it prints

Console
false true

Built as one module (-wmo), as a Release build is

Console
true true

Two 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:

Trip.swift
struct Trip {
    var city: String
    private(set) var bookings = 0

    mutating func book() {
        bookings += 1
    }
}
main.swift, in the same app
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

Console
WritableKeyPath<Trip, Int>
99

Outside 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:

main.swift, writing directly
var trip = Trip(city: "Seattle")
let bookings = \Trip.bookings
trip[keyPath: bookings] = 99

It 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:

In SIL: main.swift's key path
// 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.

7 · from lesson 13

Inside, part 2: what the object holds

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:

The string inside a key path
print((\Place.name)._kvcKeyPathString as Any)
print((\Trip.city)._kvcKeyPathString as Any)
print(NSExpression(forKeyPath: \Place.name).keyPath)

It prints

Console
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:

From the Swift standard library's source, stdlib/public/core/KeyPath.swift (shortened) — not code to type
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:

From the Swift standard library's source, stdlib/public/core/KeyPath.swift (shortened) — not code to type
  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:

From the Swift standard library's source, stdlib/public/core/KeyPath.swift (shortened) — not code to type
  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:

From the Swift standard library's source, stdlib/public/core/KeyPath.swift (shortened) — not code to type
  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:

Pin, a class that holds a Spot
final class Pin {
    var spot = Spot(name: "Pier", coords: (lat: 47.6, lon: -122.3))
}
Which paths have an offset?
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

Console
Optional(24)
Optional(0)
nil
nil
From the Swift standard library's source, stdlib/public/core/KeyPath.swift (shortened) — not code to type
  @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.

8 · from lesson 13

Which property: an offset, or what leads to one

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:

candidates: every value of one type inside a value
// 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
    }
}
With only the two types
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

Console
[7, 5]
5 24

The third thing is in the object. Read the first step each object holds, as a number:

firstStep: the first step in a key path object
// 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
}
Two paths of one type
let paths: [KeyPath<Trip, Int>] = [\.id, \.nights]
for path in paths {
    print(path, firstStep(path))
}

It prints

Console
\Trip.id 0x1000000
\Trip.nights 0x1800018

In 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>:

Trio, and two key paths in generic code
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
}
In SIL: the patterns the two functions hold
// 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:

fieldOffsets: the offsets in Trio's metadata
// 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)
    }
}
One compiled function per path, two types
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

Console
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:

A path into a Foundation struct
let start = \DateInterval.start
print(type(of: start), firstStep(start), MemoryLayout<DateInterval>.offset(of: start) as Any)
In SIL: what your code holds for start
// 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

Console
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 propertyYour compiled code holdsThe object holds
stored, in a layout the compiler knowsits offsetits offset
stored, in generic codeits entry in the type's metadataits offset, copied from that entry
of another library's typethat library's property descriptor, a getter and a setterwhat the descriptor says — here, an offset
computedits getter and setterthe 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.

9 · from lesson 13

What [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:

From the Swift standard library's source, stdlib/public/core/KeyPath.swift (shortened) — not code to type
@_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:

Three generic functions
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
}
Called with \.city and \.nights
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

Console
Victoria 6
In SIL: what each [keyPath:] became
// 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_1

A 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:

Literal key paths
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

Console
Victoria
6
In SIL, in a Debug build: every key path in main, and every line about city
// 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_1

The 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:

From the Swift compiler's source, SwiftCompilerSources/Sources/Optimizer/FunctionPasses/SimplificationPasses.swift — not code to type
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:

From the Swift compiler's source, SwiftCompilerSources/Sources/Optimizer/InstructionSimplification/SimplifyApply.swift (shortened) — not code to type
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:

From the Swift compiler's source, lib/SILOptimizer/SILCombiner/SILCombinerApplyVisitors.cpp (shortened) — not code to type
/// 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:

A function that adds a night
@inline(never)          // keeps the function whole, so that its SIL can be read
func extend(_ trip: inout Trip) {
    trip[keyPath: \Trip.nights] += 1
}
In SIL, in a Release build: what is left of the key path in extend(_:)
// 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.

10 · from lesson 13

@dynamicMemberLookup, in full

The 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:

Settings, looked up by name
@dynamicMemberLookup
struct Settings {
    var values = ["theme": "dark"]

    subscript(dynamicMember name: String) -> String? {
        values[name]
    }
}
A setting, and a typo
let settings = Settings()
print(settings.theme as Any)
print(settings.theem as Any)

It prints

Console
Optional("dark")
nil

settings.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:

A second subscript, in an extension of lesson 13's Ref
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]
    }
}
Three members
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

Console
Ref<String>
Int
Int

city 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.

Reading, with a property called value
struct Reading {
    var value: Int
}
A Ref<Reading>
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

Console
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.

11 · from lesson 13

Where SwiftUI takes a 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 writeSwiftUI takesWhat for
$trip.cityBinding's subscript(dynamicMember:): a WritableKeyPath<Value, Subject>a binding to one field — it must be able to write
$store.lastOpeneda ReferenceWritableKeyPath, in Bindable's subscript and in the $ of @ObservedObject and @EnvironmentObjecta 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:

From the iOS 27 SDK's SwiftUICore interface (shortened) — not code to type
@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:

Key paths into EnvironmentValues
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

Console
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:

A sheet that passes its dismiss on
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.)

12 · from lesson 13

What a key path cannot reach

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:

The async property
struct Forecast {
    var today: String {
        get async {
            "sun"
        }
    }
}
The lazy property
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.

Flash cards

20 cards, for this guide only: they are not in lesson 13's deck. Answer in your head first, then tap the card.

Syntax · Key pathsBefore Swift 4, what was a key path — and what did reading through one give you?
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)

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.

Syntax · Key pathsWhat decides whether a key path is a KeyPath, a WritableKeyPath or a ReferenceWritableKeyPath?
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))

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>.

Trick question · Key pathsHolder is a struct, and holder a let. Holder.store holds an object. Can you write through \Holder.store.trip.city?
let holder = Holder(store: Store())
holder[keyPath: \Holder.store.trip.city] = "Victoria"
print(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.

Trick question · Key pathsbookings is private(set), and main.swift is outside its file. What does type(of: \Trip.bookings) print there?
// Trip.swift
struct Trip {
    var city: String
    private(set) var bookings = 0

    mutating func book() {
        bookings += 1
    }
}

// main.swift
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)

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.

Syntax · Key pathsWrite key paths through a subscript, an optional chain, a forced unwrap, a dictionary key and a tuple element.
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])

\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)…

Trick question · Key pathsAtlas has a subscript that takes a Place, a plain struct. Can a key path go through it?
struct Place {
    var name: String
}

struct Atlas {
    subscript(place: Place) -> String {
        place.name.uppercased()
    }
}

let seattle = \Atlas[Place(name: "Seattle")]

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.

Syntax · Key pathsJoin \Trip.stops and \[String].count into one key path. What kind is it?
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])

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>.

Syntax · Key pathsWhat does the compiler make of trips.map(\.city)?
let trips = [Trip(id: 1, city: "Seattle", nights: 5), Trip(id: 2, city: "Victoria", nights: 1)]
print(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"].

Trick question · Key pathslet city = \Trip.city. Does trips.map(city) compile?
let trips = [Trip(id: 1, city: "Seattle", nights: 5)]
let city = \Trip.city
let cities = trips.map(city)

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] }.

Trick question · Key paths\Trip.stops[0] == \Trip.stops[0] is true. Is \Trip.stops[0] === \Trip.stops[0]?
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])
}

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.

Trick question · Key pathsMemoryLayout<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?
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)

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.

Syntax · Key pathsIn a generic function, what do a read, an assignment and a += through a key path compile to?
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
}

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.

Trick question · Dynamic member lookupSettings is @dynamicMemberLookup, and its subscript takes a String. What does the typo settings.theem do?
@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 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.

Syntax · Dynamic member lookupRef has a dynamic-member subscript for WritableKeyPaths. Add one for read-only members, so that tripRef.id works.
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]
    }
}

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.

Trick question · Dynamic member lookupReading has a property called value — and so does Ref. If ref is a Ref<Reading>, what is ref.value?
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]))

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>.

Syntax · Key paths@Environment(\.colorScheme) takes a key path. Into what — and can you use it outside a view?
import SwiftUI

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.

Trick question · Key pathsCan a view pass its dismiss action down with .environment(\.dismiss, dismiss)?
import SwiftUI

struct Sheet: View {
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        Text("Done")
            .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.

Trick question · Key paths\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?
// 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)!)

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.

Syntax · Key pathsWhat does the object of \Trip.nights hold that its type, WritableKeyPath<Trip, Int>, does not?
// 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
}

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.

Trick question · Key pathsIn generic code, \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?
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
}

// 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)

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.

Further reading