KeyPath — to typowana ścieżka do właściwości obiektu w Swift, reprezentowana jako wartość pierwszoklasowa. W przeciwieństwie do kluczy tekstowych w KVC (Key-Value Coding), KeyPath jest sprawdzany przez kompilator na etapie kompilacji: kompilator zna typ obiektu głównego, typ właściwości i może zagwarantować, że ścieżka istnieje. KeyPath jest używany w Combine do reaktywnego wiązania, w SwiftUI do dwukierunkowego bindowania oraz w bibliotece standardowej do sortowania i filtrowania kolekcji. Według Apple Developer, 2025, KeyPath jest fundamentalnym blokiem konstrukcyjnym programowania funkcyjnego w Swift.
Najważniejsze
KeyPath — to typ z biblioteki standardowej Swift, reprezentujący ścieżkę do właściwości obiektu. W przeciwieństwie do ręcznego dostępu do właściwości przez kropkę (object.property) lub klucza tekstowego (value(forKey:)), KeyPath jest obiektem pierwszej klasy: można go przekazywać jako argument, przechowywać w zmiennej i łączyć.
KeyPath zapisuje się przez odwrotny ukośnik i ścieżkę do właściwości: \Person.name. Ścieżka może być zagnieżdżona: \Person.address.city. Kompilator sprawdza, że Person ma właściwość name i że name jest rzeczywiście typu String. Jeśli właściwość nie istnieje lub typ jest nieprawidłowy, kod nie kompiluje się.
struct Person: Codable {
let name: String
let age: Int
var isActive: Bool
}
// KeyPath (tylko do odczytu)
let namePath: KeyPath<Person, String> = \Person.name
let person = Person(name: "Alice", age: 30, isActive: true)
let name = person[keyPath: namePath]
Dostęp do właściwości przez KeyPath odbywa się przez subskrypt object[keyPath: path]. Dla właściwości ReadOnly używany jest KeyPath. Jeśli właściwość jest mutowalna (var), dostępny jest WritableKeyPath. Dla typów referencyjnych — ReferenceWritableKeyPath, umożliwiający zmianę właściwości przez sam KeyPath.
KeyPath ma hierarchię podtypów odzwierciedlającą poziom dostępu do właściwości. Podstawowa hierarchia: AnyKeyPath → PartialKeyPath → KeyPath → WritableKeyPath → ReferenceWritableKeyPath. Każdy podtyp dodaje możliwości.
KeyPath — tylko do odczytu. WritableKeyPath — odczyt i zapis dla typów wartościowych (struct). ReferenceWritableKeyPath — odczyt i zapis dla typów referencyjnych (class). Swift automatycznie wybiera odpowiedni podtyp na podstawie kontekstu.
| Typ | Dostęp | Typ obiektu | Modyfikacja |
|---|---|---|---|
| KeyPath | ReadOnly | Dowolny | Nie |
| WritableKeyPath | ReadWrite | Value-typ (inout) | Przez kontekst mutating |
| ReferenceWritableKeyPath | ReadWrite | Reference-typ | Bezpośrednie przypisanie |
// WritableKeyPath dla typu wartościowego
var mutablePerson = Person(name: "Bob", age: 25, isActive: false)
let writablePath: WritableKeyPath<Person, Bool> = \Person.isActive
mutablePerson[keyPath: writablePath] = true
// ReferenceWritableKeyPath dla klasy
class User: NSObject {
@objc dynamic var name: String = ""
}
let user = User()
let refPath: ReferenceWritableKeyPath<User, String> = \User.name
user[keyPath: refPath] = "Charlie"
Ważna różnica: dla typów wartościowych ustawienie przez KeyPath wymaga var (kontekst inout), a dla typów referencyjnych — mutowalnej właściwości. Odpowiada to ogólnej semantyce Swift: typy wartościowe są przekazywane przez wartość, więc zmiana wymaga mutowalności kontenera.
KeyPath odgrywa centralną rolę w Combine i SwiftUI. W Combine operator assign używa KeyPath do wiązania wartości wydawcy z właściwością obiektu. W SwiftUI Binding używa KeyPath do dwukierunkowego powiązania modelu i widoku.
Operator assign(to:on:) przyjmuje ReferenceWritableKeyPath i obiekt. Gdy wydawca wysyła wartość, jest ona automatycznie zapisywana we wskazanej właściwości. Jest to deklaratywny sposób zarządzania stanem bez ręcznych przypisań.
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)
}
}
W SwiftUI @Binding używa KeyPath do połączenia nadrzędnego i podrzędnego View. Rodzic przekazuje Binding
KeyPath pozwala pisać elastyczne i bezpieczne typowo funkcje sortowania i filtrowania. Zamiast przekazywać domknięcie za każdym razem, można przekazać KeyPath do właściwości, według której należy sortować kolekcję. To czyni kod czystszym i zmniejsza powielanie.
Funkcja 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)
Taka implementacja sortuje według dowolnej Comparable-właściwości bez pisania domknięć. Do sortowania według nazwiska wystarczy zmienić KeyPath: \Person.lastName. Jeśli trzeba sortować według niestandardowego klucza (np. długość nazwy), używa się map wewnątrz — programista nadal może napisać własne domknięcie dla niestandardowych przypadków.
AnyKeyPath — to typ bazowy w hierarchii KeyPath, który zaciera informację o konkretnych typach korzenia i wartości. AnyKeyPath nie wie, na jakim typie obiektu jest wywoływany i jaki typ wartości zwraca. Jest to przydatne do przechowywania różnorodnych KeyPath w kolekcjach i do refleksji.
AnyKeyPath jest używany, gdy trzeba przechowywać listę ścieżek do różnych właściwości różnych typów. Na przykład w konfiguratorze UI, gdzie każda ścieżka konfiguruje określoną właściwość, a typy właściwości mogą się różnić. AnyKeyPath umożliwia jednolite działanie ze wszystkimi ścieżkami.
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 { }
// Umożliwia konfigurowanie właściwości przez AnyKeyPath
AnyKeyPath wymaga rzutowania typów przy pobieraniu wartości, ponieważ konkretny typ jest zatarty. Dla pełnego bezpieczeństwa typów zawsze preferowany jest typowany KeyPath
KeyPath jest znacznie wydajniejszy od kluczy tekstowych KVC (value(forKey:)). Klucze tekstowe są obliczane przez mechanizm runtime Objective-C, obejmujący parsowanie ciągu, wyszukiwanie w runtime i rzutowanie typów. KeyPath to mechanizm statyczny oparty na natywnych strukturach Swift.
Według testów, KeyPath jest średnio 10-20 razy szybszy od value(forKey:) przy dostępie do właściwości. Różnica wynika z braku wyszukiwania w runtime i dynamicznej dyspatching. KeyPath bezpośrednio odwołuje się do offsetu właściwości w pamięci przy wystarczającej optymalizacji kompilatora.
func readWithKeyPath(person: Person) -> String {
person[keyPath: \Person.name]
}
func readWithKVC(person: Person) -> String {
person.value(forKey: "name") as! String
}
KVC jest nie tylko wolniejszy, ale także niebezpieczny typowo: ciąg „name" może nie istnieć, a rzutowanie as! String może upaść. KeyPath gwarantuje istnienie właściwości i poprawność typu na etapie kompilacji. Dla krytycznych pod względem wydajności miejsc (animacje, lista tysięcy elementów) KeyPath jest jedynym prawidłowym wyborem.
Często zadawane pytania
KeyPath — to wskaźnik na właściwość obiektu, który można przekazywać jak zwykłą wartość. Zamiast pisać person.name, tworzysz ścieżkę \Person.name i używasz jej do odczytu lub zapisu właściwości w dowolnym egzemplarzu Person.
KeyPath — tylko do odczytu. WritableKeyPath — do odczytu i zapisu typów wartościowych (struct) przez kontekst inout. ReferenceWritableKeyPath — do odczytu i zapisu typów referencyjnych (class) bez dodatkowych warunków.
KeyPath jest szeroko używany w Combine (assign(to:on:)), SwiftUI (Binding, FocusState), sortowaniu kolekcji (sorted(by:)), Core Data (NSSortDescriptor) oraz do bezpiecznego dostępu do zagnieżdżonych właściwości.
Tak, KeyPath jest w pełni bezpieczny typowo. Błąd w ścieżce (literówka, nieprawidłowy typ) jest wykrywany na etapie kompilacji, a nie w runtime. value(forKey:) używa ciągów znaków i może upaść z NSUndefinedKeyException, jeśli klucz nie istnieje.
Bezpośrednio — nie, Swift nie obsługuje dynamicznego tworzenia KeyPath z ciągu znaków. Można jednak użyć AnyKeyPath do przechowywania i przekazywania KeyPath uzyskanych statycznie. Do dynamicznego dostępu do właściwości po ciągu używaj value(forKey:) z Objective-C runtime.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również