KeyPath — це типізований шлях до властивості об’єкта в Swift, представлений як значення першого класу. На відміну від рядкових ключів у KVC (Key-Value Coding), KeyPath перевіряється компілятором на етапі збіірки: компілятор знає тип кореневого об’єкта, тип самої властивості і може гарантувати, що шлях існує. KeyPath використовується в Combine для реактивного зв’язування, у SwiftUI для двостороннього зв’язування і в стандартній бібліотеці для сортування і фільтрації колекцій. Згіідно з Apple Developer, 2025, KeyPath є фундаментальним будівельним блоком для функціонального програмування в Swift.
Головне
KeyPath — це тип зі стандартної бібліотеки Swift, що представляє шлях до властивості об’єкта. На відміну від ручного доступу до властивості через крапку (object.property) або рядкового ключа (value(forKey:)), KeyPath є об’єктом першого класу: його можна передавати як аргумент, зберігати в змінній і комбіінувати.
KeyPath записується через зворотний слеш і шлях до властивості: \Person.name. Шлях може бути вкладеним: \Person.address.city. Компілятор перевіряє, що Person має властивість name і що name дійсно String. Якщо властивість не існує або тип неправильний, код не компілюється.
struct Person: Codable {
let name: String
let age: Int
var isActive: Bool
}
// KeyPath (лише читання)
let namePath: KeyPath<Person, String> = \Person.name
let person = Person(name: "Alice", age: 30, isActive: true)
let name = person[keyPath: namePath]
Доступ до властивості через KeyPath здійснюється через сабскрипт object[keyPath: path]. Для властивостей лише для читання використовується KeyPath. Якщо властивість зміінювана (var), доступний WritableKeyPath. Для посилальних типів ReferenceWritableKeyPath дозволяє зміінювати властивість безпосередньо через KeyPath.
KeyPath має єрархію підтипів, що відображають ріівень доступу до властивості. Базова єрархія: AnyKeyPath → PartialKeyPath → KeyPath → WritableKeyPath → ReferenceWritableKeyPath. Кожен підтип додає можливості.
KeyPath — лише читання. WritableKeyPath — читання та запис для типів-значень (struct). ReferenceWritableKeyPath — читання та запис для посилальних типів (class). Swift автоматично вибирає правильний підтип на основі контексту.
| Тип | Доступ | Тип об’єкта | Зміна |
|---|---|---|---|
| KeyPath | Лише читання | Будь-який | Ні |
| WritableKeyPath | Читання та запис | Тип-значення (inout) | Через mutating контекст |
| ReferenceWritableKeyPath | Читання та запис | Посилальний тип | Пряме присвоєння |
// WritableKeyPath для типу-значення
var mutablePerson = Person(name: "Bob", age: 25, isActive: false)
let writablePath: WritableKeyPath<Person, Bool> = \Person.isActive
mutablePerson[keyPath: writablePath] = true
// ReferenceWritableKeyPath для класу
class User: NSObject {
@objc dynamic var name: String = ""
}
let user = User()
let refPath: ReferenceWritableKeyPath<User, String> = \User.name
user[keyPath: refPath] = "Charlie"
Важлива відмінність: для типів-значень встановлення через KeyPath вимагає var (inout-контекст), а для посилальних типів — лише зміінюваної властивості. Це відповідає загальній семантиці Swift: типи-значення передаються за значенням, тому змііна вимагає зміінюваності контейнера.
KeyPath відіграє центральну роль у Combine та SwiftUI. У Combine оператор assign використовує KeyPath для прив’язки значення видавця до властивості об’єкта. У SwiftUI Binding використовує KeyPath для двостороннього зв’язку між моделлю та відображенням.
Оператор assign(to:on:) приймає ReferenceWritableKeyPath та об’єкт. Коли видавець надсилає значення, воно автоматично записується у вказану властивість. Це декларативний спосіб керування станом без ручних присвоєнь.
import Combine
class SettingsViewModel: ObservableObject {
@Published var volume: Float = 0.5
var cancellables = Set<AnyCancellable>()
func bindSlider(publisher: AnyPublisher<Float, Never>) {
publisher
.assign(to: \SettingsViewModel.volume, on: self)
.store(in: &cancellables)
}
}
У SwiftUI @Binding використовує KeyPath для зв’язку батьківського та дочіірнього відображень. Батько передає Binding
KeyPath дозволяє писати гнучкі та типобезпечні функції сортування і фільтрації. Замість того щоб передавати замикання щоразу, можна передати KeyPath до властивості, за якою потрібно відсортувати колекцію. Це робить код чистіішим і зменшує дублювання.
Функція sorted
extension Sequence {
func sorted<Value: Comparable>(
by keyPath: KeyPath<Element, Value>,
ascending: Bool = true
) -> [Element] {
ascending
? self.sorted { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
: self.sorted { $0[keyPath: keyPath] > $1[keyPath: keyPath] }
}
}
let sortedByName = people.sorted(by: \Person.name)
let sortedByAgeDesc = people.sorted(by: \Person.age, ascending: false)
Така реалізація сортує за будь-якою Comparable-властивістю без написання замикань. Для сортування за прізвищем достатньо зміінити KeyPath: \Person.lastName. Якщо потрібно сортувати за кастомним ключем (наприклад, довжина імені), використовується map всередині — розробник все ще може написати своє замикання для нестандартних випадків.
AnyKeyPath — це базовий тип в єрархії KeyPath, який стирає інформацію про конкретні типи кореня та значення. AnyKeyPath не знає, на якому типі об’єкта він викликається і який тип значення повертає. Це корисно для зберігання різнорідних KeyPath у колекціях і для рефлексії.
AnyKeyPath використовується, коли потрібно зберігати список шляхів до ріізних властивостей ріізних типів. Наприклад, у конфігураторі UI, де кожен шлях налаштовує певну властивість, і типи властивостей можуть відрізнятися. AnyKeyPath дозволяє єдиноподібно працювати зі всііма шляхами.
protocol Configurable {
func apply(_ keyPath: any PartialKeyPath<Self>, value: any)
}
extension Configurable {
func configure(_ pairs: (any KeyPath<Self, any>, any)...) {
for (path, value) in pairs {
apply(path, value: value)
}
}
}
extension Person: Configurable { }
// Дозволяє налаштовувати властивості через AnyKeyPath
AnyKeyPath вимагає приведення типів при вилученні значення, оскільки конкретний тип стерто. Для повної безпеки типів завжди краще використовувати типізований KeyPath
KeyPath значно продуктивніший за рядкові ключі KVC (value(forKey:)). Рядкові ключі обчислюються через runtime-механізм Objective-C, включаючи розбір рядка, пошук у рантаймі та приведення типів. KeyPath — статичний механізм, заснований на нативних структурах Swift.
За даними тестів, KeyPath у середньому в 10-20 разів швидший за value(forKey:) для доступу до властивостей. Різниця обумовлена відсутністю runtime-пошуку та динамічної диспетчеризації. KeyPath безпосередньо звертається до offset-у властивості в пам’яті при достатній оптимізації компілятора.
func readWithKeyPath(person: Person) -> String {
person[keyPath: \Person.name]
}
func readWithKVC(person: Person) -> String {
person.value(forKey: "name") as! String
}
KVC не тільки повільніший, але і небезпечний за типами: рядок “name” може не існувати, а приведення as! String може впасти. KeyPath гарантує існування властивості та коректність типу на етапі компііляції. Для критичних за продуктивністю діілянок (анімації, списки на тисячі елементів) KeyPath — єдиний правильний вибіір.
Часто задавані питання
KeyPath — це вказівник на властивість об’єкта, який можна передавати як звичайне значення. Замість того щоб писати person.name, ви створюєте шлях \Person.name і використовуєте його для читання або запису властивості в будь-якому екземплірі Person.
KeyPath — лише для читання. WritableKeyPath — для читання та запису value-типів (struct) через inout-контекст. ReferenceWritableKeyPath — для читання та запису посилальних типів (class) без додаткових умов.
KeyPath широко використовується в Combine (assign(to:on:)), SwiftUI (Binding, FocusState), сортуванні колекцій (sorted(by:)), Core Data (NSSortDescriptor) і для безпечного доступу до вкладених властивостей.
Так, KeyPath повністю типобезпечний. Помилка в шляху (друк, неправильний тип) виявляється на етапі компііляції, а не в рантаймі. value(forKey:) використовує рядки і може впасти з NSUndefinedKeyException, якщо ключ не існує.
Напряму — ні, Swift не підтримує динамічне створення KeyPath з рядка. Однак можна використовувати AnyKeyPath для зберігання та передачі KeyPath, отриманих статично. Для динамічного доступу до властивостей за рядком використовується value(forKey:) з Objective-C runtime.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.