https://developer.apple.com/documentation/swift/keypath
KeyPath | Apple Developer Documentation
A key path from a specific root type to a specific resulting value type.
developer.apple.com
정의
Swift에서 KeyPath는 객체의 프로퍼티에 type safety하게 접근하기 위한 경로이다.
간단한 예시를 살펴보자.
import Foundation
struct User {
let name: String
let age: Int
}
let user = User(name: "Liam", age: 30)
let name = user[keyPath: \User.name] // "Liam"
let age = user[keyPath: \User.age] // 30
조금 더 살펴보자면 KeyPath는 제네릭 타입이다.
public class KeyPath<Root, Value> : PartialKeyPath<Root> {}
KeyPath는 제네릭 타입으로 이를 이용하면 Root 타입에서 특정 프로퍼티의 Value 타입에 안전하게 접근할 수 있다.
struct User {
let name: String
let age: Int
}
let nameKeyPath: KeyPath<User, String> = \User.name
let ageKeyPath: KeyPath<User, Int> = \User.age
let user = User(name: "Liam", age: 30)
let name = user[keyPath: nameKeyPath] // "Liam"
let age = user[keyPath: ageKeyPath] // 30
Key-Path Expression
위 예시에서 살펴본 것 처럼, KeyPath 타입의 인스턴스를 만드는 가장 일반적인 방법은 \SomeClass.someProperty 같은 키-패스 표현식을 사용하는 것이다.
Documentation
docs.swift.org
키-패스 표현식은 특정 타입의 프로퍼티 또는 서브스크립트를 참조한다. 이 경로를 통해 타입 내부 데이터를 동적으로, 안전한게 다룰 수 있다.
기본 형태
주로 KVO같은 동적 프로그래밍 작업에서 사용되는데 아래와 같은 형태를 갖는다.
\<#type name#>.<#path#>
type name은 구체적인 타입 이름으로 제네릭 파라미터도 포함할 수 있다.
path는 프로퍼티 명, 서브스크립트, 옵셔널 체이닝, 강제 언래핑 표현식으로 구성된다. 이러한 키-패스 구성 요소는 필요한 만큼 반복해서, 어떤 순서로든 사용할 수 있다.
(아래는 가능한 표현식의 여러 예제들)
let path: KeyPath<[String], String> = \[String].[1]
let path2: KeyPath<Set<Int>, Int?> = \Set<Int>.first
struct User {
let name: String
let age: Int
let address: Address?
}
struct Address {
let city: City
}
struct City { }
let path3: KeyPath<User, City?> = \User.address?.city
struct SomeType {
let dict: Dictionary<String, String>? = [:]
}
let path4: KeyPath<SomeType, Int?> = \SomeType.dict!["key"]?.count
Syntactic sugar
컴파일 타임에 키-패스 표현식은 KeyPath 클래스의 인스턴스로 대체된다.
명확한 확인을 위해 타입을 명시해줬는데, 제일 처음 살펴본 예제처럼 값을 할당해주지 않아도 된다. (syntactic sugar)
그리고 여태 사용했던 것 처럼 값을 KeyPath를 사용해 접근하려면 subscript(keyPath:)를 이용하면 된다. (모든 타입에서 사용 가능)
import Foundation
struct User {
let name: String
let age: Int
}
let user = User(name: "Liam", age: 30)
let name = user[keyPath: \User.name] // "Liam"
let age = user[keyPath: \User.age] // 30
type name 추론 가능
type name이 추론 가능한 경우 생략할 수 있다.
class SomeClass: NSObject {
@objc dynamic var someProperty: Int
init(someProperty: Int) {
self.someProperty = someProperty
}
}
let c = SomeClass(someProperty: 10)
c.observe(\.someProperty) { object, change in // SomeClass 생략함.
// ...
}
path는 self 참조 가능
path는 self를 참조할 수도 있다. \.self (전체 인스턴스를 참조하는 키패스)
var compoundValue = (a: 1, b: 2)
// Equivalent to compoundValue = (a: 10, b: 20)
compoundValue[keyPath: \.self] = (a: 10, b: 20)
서브스크립트
1) path에 사용하는 서브스크립트의 파라미터는 Hashable을 준수해야 한다.
이유는 따로 나와있지 않는데, KeyPath 자체가 Hashable을 준수해야 하고,
이 값을 비교하거나 딕셔너리의 key처럼 저장하거나, Set에 넣거나 등등이 가능하기 때문에
그 내부에 들어가는 서브스크립트 파라미터도 Hashable해야 하는듯.
2) 서브스크립트에 사용되는 값은 이름 있는 변수(named value)나 상수(literal)이 될 수 있다.
변수를 사용하는 경우, KeyPath에 사용하는 서브스크립트가 변수를 캡처할 때 값이 복사된다는 점을 유의해야 한다.
즉 해당 변수가 이후에 변경이 되더라도, 캡처 당시의 값이 고정된다는 말이다. (클로저는 reference capture)
let greetings = ["hello", "hola", "bonjour", "안녕"]
var index = 2
let path = \[String].[index]
let fn: ([String]) -> String = { strings in strings[index] }
print(greetings[keyPath: path]) // bonjour
print(fn(greetings)) // bonjour
index += 1
print(greetings[keyPath: path]) // bonjour
print(fn(greetings)) // 안녕
함수나 클로저처럼 사용
키-패스 표현식을 함수나 클로저 대신 사용할 수 있다. (Root) -> Value 형태인 경우.
struct Task {
var description: String
var completed: Bool
}
var toDoList = [
Task(description: "Practice ping-pong.", completed: false),
Task(description: "Buy a pirate costume.", completed: true),
Task(description: "Visit Boston in the Fall.", completed: false),
]
// Both approaches below are equivalent.
let descriptions = toDoList.filter(\.completed).map(\.description)
let descriptions2 = toDoList.filter { $0.completed }.map { $0.description }
Side Effect
키-패스 표현식에 포함된 side effect는 해당 표현식이 평가될 때 단 한번만 실행된다.
아래 예시처럼 서브스크립트 안에서 함수를 호출한다면, 해당 함수는 단 한번만 호출된다.
KeyPath가 사용될 때마다 반복해서 실행되는 것이 아니라, 해당 표현식이 평가될 때만 실행
func makeIndex() -> Int {
print("Made an index")
return 0
}
// The line below calls makeIndex().
let taskKeyPath = \[Task][makeIndex()]
// Prints "Made an index"
// Using taskKeyPath doesn't call makeIndex() again.
let someTask = toDoList[keyPath: taskKeyPath]
등장 배경
Swift 4.0에 추가된 문법이다.
https://github.com/swiftlang/swift-evolution/blob/main/proposals/0161-key-paths.md
swift-evolution/proposals/0161-key-paths.md at main · swiftlang/swift-evolution
This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution
github.com
기존에 KVO 방식에서 프로퍼티 경로를 참조하거나 전달하려면 String 기반 접근 방식 (feat. #keyPath)을 사용했다.
이는 아래와 같은 제한 사항이 있다.
- 타입 정보가 손실되므로, 결국 Any 기반 API를 써야 하는 불편함
- 문자열이기 때문에, 런타임에 파싱해야 해서 느림
- #keyPath()는 NSObject를 상속받는 클래스에만 사용 가능
- Darwin (iOS/macOS) 플랫폼에서만 사용할 수 있음
또한 기존 문법에서 프로퍼티 자체를 메서드처럼 전달하거나 추상하기 어려워 KeyPath 문법이 등장.
KeyPath 종류

AnyKeyPath
- Root, Value 타입이 지워짐
- KeyPath끼리 저장하거나 비교할 때 사용
- 실사용보다는 컨테이너 목적
PartialKeyPath<Root>
- 읽기 전용
- Value 타입을 모를 때 사용
- Root 타입은 알지만, Value 타입은 알 수 없음
KeyPath<Root, Value>
- 읽기 전용
- 값 타입, 참조 타입 모두 사용 가능
WritableKeyPath<Root, Value>
- 읽기 + 쓰기 가능
- 값 타입에서 사용
- 해당 프로퍼티가 var이어야 가능
ReferenceWritableKeyPath<Root, Value>
- 읽기 + 쓰기 가능
- 참조 타입에서만 사용 가능
- 해당 프로퍼티가 var이어야 가능
활용 예제 모음
[Swift] KeyPath 유용한 예제 모음
KeyPath 를 SwiftUI 쪽에서 자주 쓰면서 Swift 코드에도 자주 사용하고 싶어서 정리 및 useful example 을 모아두려고 한다. 사실.. 예를들어 map(\.xx) 이런 코드 많이 축약됐네~ 이런 느낌도 잘안들고 익숙하
eunjin3786.tistory.com
'iOS > Swift' 카테고리의 다른 글
Swift 공식문서 9. Structures and Classes (1) | 2024.10.05 |
---|---|
Swift 공식문서 8. Enumeration (0) | 2024.10.03 |
Swift 공식문서 7. Closures (3) | 2024.09.26 |
Swift 공식문서 6. Functions (0) | 2024.09.20 |
Swift - In-Out 파라미터 (0) | 2024.09.20 |