Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Archives
Today
Total
관리 메뉴

sanichdaniel의 iOS 개발 블로그

KeyPath 본문

swift

KeyPath

sanich8355 2020. 10. 3. 12:53
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/

 

The power of key paths in Swift | Swift by Sundell

Swift keeps gaining more and more features that are more dynamic in nature, while still retaining its focus on type safe code. This week, let’s take a look at how key paths in Swift work, and some of the cool and powerful things they can let us do.

www.swiftbysundell.com

www.hackingwithswift.com/articles/57/how-swift-keypaths-let-us-write-more-natural-code

 

How Swift keypaths let us write more natural code

Combine keypaths, associated types, and generics in one

www.hackingwithswift.com

 

'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