sanichdaniel의 iOS 개발 블로그
KeyPath 본문
Property Wrapper 내부에서 enclosing self 를 접근하는 방법으로 KeyPath를 이용하는 해결방안이 있었는데
KeyPath가 뭐지.. 알아보자!
정의
KeyPath는 타입의 프로퍼티 값을 읽거나, 수정하지 않고 참조하는 방법을 말합니다.
Swift 4 전까지는 KeyPath를 사용하려면 클로져로 래핑하거나, #keyPath() 문법을 써야했으나, 4부터는 사용하기 편리하도록 바뀌었다.
struct Person {
let name: String
func greet() {
print("Hello \(name)!")
}
}
let p = Person(name: "Samus")
let greeter = p.greet // stores the method without evaluating it.
greeter() // calls the stored method
// this is the only way in Swift 3.1 and below to defer evaluating the name property of a Person.
let getName = { (p: Person) in p.name }
print(getName(p)) // evaluate the property
Swift4 부터는 아래처럼 사용 가능하다
let getName = \Person.name
print(p[keyPath: getName])
// or just this:
print(p[keyPath: \Person.name])
\Person.name 은 KeyPath<Person, String>를 만들수 있는 길이다.
첫번째 인자는 root type이고 두번째 인자는 우리가 원하는 프로퍼티의 타입이다.
nested된 프로퍼티도 접근 가능하다
ex) \Person.name.count
KeyPath 위계
AnyKeyPath
|
v
PartialKeyPath<Root>
|
v
KeyPath<Root, Value>
|
v
WritableKeyPath<Root, Value>
|
v
ReferenceWritableKeyPath<Root, Value>
PartialKeyPath<Root>는 root만 가지고 value타입의 값은 컴파일 타임에 모른다.
RootType이 같고 Value 타입이 다른 어레이를 만들때 유용하다
struct Person {
var name: String
let birthdate: Date
}
let person = Person(name: "DongUk", birthdate: Date())
let kp: PartialKeyPath<Person> = \Person.name
let name = person[keyPath: kp]
let personPaths: [PartialKeyPath<Person>] = [
\Person.name,
\Person.birthdate,
]
name의 컴파일 타임 타입은 Any 이고
런타임 타입은 String이다.
KeyPath
read-only
WritableKeyPath<Person, String>
데이터를 수정할 수 있으나 value 타입의 var 프로퍼티에 한정
만약 name이 var 이었다면
let kp = \Person.name
var p = Person(name: "Samus")
p[keyPath: kp] = "Ridley"
kp의 타입은 WritableKeyPath<Person, String> 가 된다.
ReferenceWritableKeyPath
데이터를 수정할 수 있으나 reference타입의 var 프로퍼티에 KeyPath를 사용했을때 사용된다
KeyPath 더하기
A 키의 Value 타입과 B 타입의 Root 타입이 같을 경우 KeyPath 더하기 가능하다
let accountOwnerPath = \BankAccount.owner
let namePath = \Person.name
let accountOwnerNamePath = accountOwnerPath.appending(namePath) // returns KeyPath<BankAccount, String>
let account: BankAccount = ...
account[keyPath: accountOwnerNamePath] // returns the name of the account owner
KeyPath 사용법 #1
struct Person {
var socialSecurityNumber: String
var name: String
}
struct Book {
var isbn: Int
var title: String
}
Person과 Book은 유니크한 identifier인 socialSecurityNumber, isbn 2가지를 가진다
이 둘을 가리키는 프로토콜을 선언해볼수 있다
protocol Identifiable {
var id: String { get set }
}
이 프로토콜은 identifier가 String타입일 경우에만 유효하다
어떻게 String이외의 타입의 identifier을 갖는 타입도 Identifilable을 채택하게 해줄까?
KeyPath는 이 문제를 해결해주는데, 각 데이터 타입마다 다른 어답터를 사용하게 하는것이다.
어답터 패턴: 각기 다른 타입들에 인터페이스를 래핑해서 같이 쓰게 해주는것
일단 제너릭한 프로토콜을 만들어야하니 associated type을 사용해야한다.
protocol Identifiable {
associatedtype ID
static var idKey: WritableKeyPath<Self, ID> { get }
}
struct Person: Identifiable {
static let idKey = \Person.socialSecurityNumber
var socialSecurityNumber: String
var name: String
}
struct Book: Identifiable {
static let idKey = \Book.isbn
var isbn: Int
var title: String
}
이제 제너릭하게 이용해보자!
func printID<T: Identifiable>(thing: T) {
print(thing[keyPath: T.idKey])
}
let taylor = Person(socialSecurityNumber: "555-55-5555", name: "Taylor Swift")
printID(thing: taylor)
KeyPath를 이용하여 값을 저장하는 프로퍼티와, 프로토콜을 채택한 프로퍼티를 구분하였다. (데이터의 분리)
프로토콜에 프로퍼티 이름을 굳이 맞출필요가 없는것이다.
KeyPath 사용법 #2
Stack과 유사한 데이터 구조로, lookUp과 set 함수는 인자로 KeyPath를 받는다.
현재 스택의 최상단에 있는 원소의, 프로퍼티를 수정할수 있게해준다.
KeyPath가 없었다면, 외부에서 값을 참조해서 프로퍼티를 세팅했을것 같다.
하지만 KeyPath를 통해 프로퍼티 세팅을 ScopeStack의 책임으로 만들어주었다.
class ScopeStack<Scope> {
private var stack = [Scope]()
func push(_ scope: Scope) {
stack.append(scope)
}
func pop() -> Scope? {
return stack.popLast()
}
func lookUp<T>(key: KeyPath<Scope, T>) -> T? {
return stack.last?[keyPath: key]
}
func set<T>(key: WritableKeyPath<Scope, T>, value: T) {
guard stack.count > 0 else { return }
stack[stack.count - 1][keyPath: key] = value
}
}
KeyPath 사용법 #3
(Root) -> Value 를 사용하는 함수가 있다면
\Root.value 로도 사용 가능하다
let carModels = cars.map { $0.model }
let carModels = cars.map(\.model)
KeyPath 사용법 #4
class ListViewController {
private var items = [Item]() { didSet { render() } }
func loadItems() {
loader.load { [weak self]
items in self?.items = items
}
}
}
items 프로퍼티의 setter을 인자로 넘겨주면 어떨까?
ReferenceWritableKeyPath인 이유는 reference 타입의 값에만 한정짓고 싶기 때문에
value 타입이면 함수 내에서 인자로 들어온 경우 let 타입이기에 내부 프로퍼티를 수정할수 없다
func setter<Object: AnyObject, Value>(
for object: Object,
keyPath: ReferenceWritableKeyPath<Object, Value>
) -> (Value) -> Void {
return { [weak object] value in
object?[keyPath: keyPath] = value
}
}
class ListViewController {
private var items = [Item]() { didSet { render() } }
func loadItems() {
loader.load(then: setter(for: self, keyPath: \.items))
}
}
결론
KeyPath는 프로퍼티를 동적으로 접근할수 있게 해준다.
클로져도 비슷한 효과를 얻을수 있지만, 가볍고, 선언적인 keyPath를 이용하면 편리하다!
출처
www.swiftbysundell.com/articles/the-power-of-key-paths-in-swift/
www.hackingwithswift.com/articles/57/how-swift-keypaths-let-us-write-more-natural-code
'swift' 카테고리의 다른 글
Identifiable (0) | 2020.10.07 |
---|---|
Hashable (0) | 2020.10.07 |
Generic where cause (0) | 2020.10.01 |
Opaque Types (0) | 2020.09.29 |
enum vs protocol as UseCase (0) | 2020.09.28 |