본문 바로가기

iOS/Swift

Swift - KeyPath

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 같은 키-패스 표현식을 사용하는 것이다.

 

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/expressions/#Key-Path-Expression

 

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 유용한 예제모음

 

[Swift] KeyPath 유용한 예제 모음

KeyPath 를 SwiftUI 쪽에서 자주 쓰면서 Swift 코드에도 자주 사용하고 싶어서 정리 및 useful example 을 모아두려고 한다. 사실.. 예를들어 map(\.xx) 이런 코드 많이 축약됐네~ 이런 느낌도 잘안들고 익숙하

eunjin3786.tistory.com

 

 

 

'iOS > Swift' 카테고리의 다른 글