Swift KeyPath vs #keyPath()

Apr 07, 2023#swift

In Swift, a key-path refers to a property or subscript of a type. You use key-path expressions in dynamic programming tasks, such as key-value observing.

Key paths are type-safe, which means the compiler checks that the key path you are using actually refers to a property or value that exists in the object. This helps prevent runtime errors and makes your code more reliable.

#keyPath()

A key-path string expression, #keyPath(), lets you access the string used to refer to a property in Objective-C, for use in key-value coding and key-value observing APIs. It has the following form:

#keyPath(<#property name#>)

The property name must be a reference to a property that’s available in the Objective-C runtime. At compile time, the key-path string expression is replaced by a string literal. For example:

import Foundation

class MyClass: NSObject {
    @objc var myProperty: Int
    init(myProperty: Int) {
        self.myProperty = myProperty
    }
}

let myObject = MyClass(myProperty: 42)
let myKeyPath = #keyPath(MyClass.myProperty)

if let myValue = myObject.value(forKey: myKeyPath) {
    print(myValue)
}
// Prints "42"

The #keyPath() syntax provides a convenient way to safely refer to properties. Unfortunately, once validated, the expression becomes a String which has a number of important limitations:

  • Loss of type information (requiring awkward Any APIs)
  • Unnecessarily slow to parse
  • Only applicable to NSObjects
  • Limited to Darwin platforms

KeyPaths

KeyPathss are a family of generic classes which encapsulate a property reference or chain of property references, including the type, mutability, property name(s), and ability to set/get values.

The performance of interacting with a property/subscript via KeyPaths should be close to the cost of calling the property directly.

A key-path expression takes the general form \<Type>.<path>, where <Type> is a type name, and <path> is a chain of one or more property, subscript, or optional chaining/forcing operators. If the type name can be inferred from context, then it can be elided, leaving \.<path>.

There’re several variants of KeyPath family:

  1. KeyPath provides read-only access to a property. You can use it to get the value of a property, but not to set it. For example:
struct Point {
    let x: Double
    let y: Double
}

let origin = Point(x: 0, y: 0)

// Create a keypath for the x property
let xKeyPath = \Point.x

// Use the keypath to get the x value of origin
let originX = origin[keyPath: xKeyPath]
print(originX) // 0

// Try to use the keypath to set the x value of origin
origin[keyPath: xKeyPath] = 1 // Error: Cannot assign to property: 'origin' is a 'let' constant
  1. WritableKeyPath provides read-write access to a mutable property with value semantics. You can use it to get or set the value of a property, but only if the instance is also mutable. For example:
struct Person {
    var name: String
    var age: Int
}

var alice = Person(name: "Alice", age: 25)

// Create a keypath for the name property
let nameKeyPath = \Person.name

// Use the keypath to get the name value of alice
let aliceName = alice[keyPath: nameKeyPath]
print(aliceName) // Alice

// Use the keypath to set the name value of alice
alice[keyPath: nameKeyPath] = "Bob"
print(alice.name) // Bob

// Try to use the keypath with a constant instance
let bob = Person(name: "Bob", age: 30)
bob[keyPath: nameKeyPath] = "Alice" // Error: Cannot assign to property: 'bob' is a 'let' constant
  1. ReferenceWritableKeyPath can only be used with reference types (such as instances of a class), and provides read-write access to any mutable property. You can use it to get or set the value of a property, even if the instance is a constant, as long as the property is mutable. For example:
class Book {
    var title: String
    var author: String
    
    init(title: String, author: String) {
        self.title = title
        self.author = author
    }
}

let book = Book(title: "1984", author: "George Orwell")

// Create a keypath for the title property
let titleKeyPath = \Book.title

// Use the keypath to get the title value of book
let bookTitle = book[keyPath: titleKeyPath]
print(bookTitle) // 1984

// Use the keypath to set the title value of book
book[keyPath: titleKeyPath] = "Animal Farm"
print(book.title) // Animal Farm

// Try to use the keypath with a constant property
book.author = "Eric Blair" // Error: Cannot assign to property: 'author' is a 'let' constant