KeyPath는 프로퍼티를 참조하여 읽고 쓸 수 있게 해주는 것

 

keypath 는 역슬래시( \ )와 타입, 마침표( . ) 경로로 구성되어 있습니다.

class Person {
    var name: String
   
    init(name: String) {
        self.name = name
    }
}
 
struct Stuff {
    var name: String
    var owner: Person
}
// name이 var로 선언된 경우 WritableKeyPath<Person, String>
// name이 let로 선언된 경우 KeyPath<Animal, String>.Type
print(type(of: \Person.name)) //WritableKeyPath<Person, String>
print(type(of: \Stuff.name)) //WritableKeyPath<Stuff, String>

 

keypath 는 기존 키 경로에 하위 경로를 추가하여 붙일 수 있습니다.

let keyPath = \Stuff.owner
let nameKeyPath = keyPath.appending(path: \.name)

 

그리고 각 인스턴스 KeyPath 서브 스크립트 메서드에 키 경로를 전달해 프로퍼티에 접근도 가능합니다.

struct Address {
    var town: String
}
 
struct Person {
    var address: Address
}
 
let address = Address(town: "한옥마을")
let Seogon = Person(address: address)
 
let SeogonAddress = Seogon[keyPath: \.address] //Address(town: "한옥마을")
let SeogonTown = Seogon[keyPath: \.address.town] //한옥마을

 

⌘ KeyPath 종류

⌘ 키 경로 만드는 법

WritableKeyPath

struct Person {
    var name: String
}
struct Stuff {
    var owner: Person
}
 
let writableKeypath = \Stuff.owner.name

person의 name이 var(변수)이기 때문에 변경 가능한 모든 프로퍼티에 대한 read & write access를 제공하기 때문에 writableKeyPath가 된다. 만약 name이 let 면 수정이 불가능하므로 WritableKeyPath가 아닌 일반적인 KeyPath 타입이 된다.

 

ReferenceWritableKeyPath

class Person {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

class Stuff {
    var name: String
    var owner: Person
    
    init(name: String, owner: Person) {
        self.name = name
        self.owner = owner
    }
}

let student = Person(name: "lion")
var study = Stuff(name: "pen", owner: student)

let referenceWritableKeyPath = \Stuff.name

study[keyPath: referenceWritableKeyPath] //lion

 

KeyPath를 사용했을때 이점

let student = Person(name: "lion")
var study = Stuff(name: "pen", owner: student)

let referenceWritableKeyPath = \Stuff.name
// 그냥 study.name 으로 값을 접근하면 되는거 아닌가?
study[keyPath: referenceWritableKeyPath] //lion

 

🔴 그냥 값에 . 을 사용해서 접근할 수 있지만 왜 KeyPath를 사용해야하는가! (아래 코드처럼 접근하는 것이 값에 대한 직접 참조다)

let swift: Swift = Swift()
print(swift.propertyOne) // swift 인스턴스의 propertyOne 값에 접근

 

➡️ 특별한 API를 만든다고 가정했을 때 직관적으로 알 수 있도록 접근할 수 있게 됩니다.

 

 

➡️ 코드가 간편해진다.

예를 들어 people 객체에서 name만 가져오고 싶으면 보통 map을 사용한다.

let people = [
    Person(name: "Kim", age: 25),
    Person(name: "Yoon", age: 28),
]
let names1 = people.map{ $0.name }

여기서 KeyPath를 사용해도 같은 결과를 반환한다.

let name2 = people.map(\.name)

 

➡️ 타입에 대한 의존성이 줄어든다.

아래 코드처럼 타입을 좀 더 자유롭게 사용할 수 있다

struct Cluster {
    var name: String
}

struct Cadet {
    let cluster1: Cluster
    let cluster2: Cluster
}

// KeyPath를 활용해서 Address를 가져오는 연산 프로퍼티
struct ClusterNick {
    var cadet: Cadet
    var path: KeyPath<Cadet, Cluster>

    var name: Address {
        switch path {
            case \.cluster1:
            return "멍멍1"
            case \.clutser2:
            return "멍멍2"
            case 
        }
    }
}

 

 

다음으로는 특정 데이터 모델을 정의하여 해당 데이터 모델을 정렬하기 위해 keypath를 사용하는 코드 설명입니다.

keypath를 사용하면 좋은 상황 예시

아래 내용처럼 keypath를 이용하면 좀 더 좋은 코드를 만들 수 있게 됩니다

데이터 모델

struct MyDataModel: Identifiable {
    let id = UUID().uuidString
    let title: String
    let count: Int
    let date: Date
}

 

데이터 모델을 사용하여 만든 배열

아래 array 배열을 정렬하기 위해서는 sort(by:)를 사용할 수 있지만 extension을 사용하여 새로운 정렬 메서드를 정의할 수 있습니다.

let array = [
    MyDataModel(title: "Three", count: 3, date: .distantFuture),
    MyDataModel(title: "One", count: 1, date: .now),
    MyDataModel(title: "Two", count: 2, date: .distantPast)
]
let newArray = array.sorted { item1, item2 in
//  return item1.count < item2.count
    return item1[keyPath: \.count] < item2[keyPath: \.count]
}

 

배열 정렬하기

아래처럼 구현하게 된다면 이제 MyDataModel 데이터 타입의 값이라면 customSorted() 메서드로 정렬된 값을 받을 수 있게 됩니다.

하지만 아래처럼 구현하게 된다면 count으로만 정렬된 [MyDataModel] 타입으로만 값을 반환할 수 있게 됩니다. 

extension Array where Element == MyDataModel {
    func customSorted() -> [Element] {
        self.sorted { item1, item2 in
            return item1.count < item2.count
        }
    }
}

 

그렇기 때문에 만약 count가 아닌 title로 정렬하고 싶으면 다시 코드를 바꿔줘야하거나 새로운 메서드를 정의해야 하는데 이는 매우 비효율적입니다.

extension Array where Element == MyDataModel {
    func customSorted2() -> [Element] {
        self.sorted { item1, item2 in
            return item1.title < item2.title
        }
    }
}

 

이런 상황에서 Keypath를 사용하면 유용합니다, 만약 아래처럼 keyPath를 사용하게 된다면 MyDataModel 인스턴스에서 Compareble 프로토콜을 준수한다면 어떤 프로퍼티로도 정렬이 가능해지는 코드를 만들 수 있게 됩니다.

extension Array where Element == MyDataModel {
    func customSorted<T: Comparable>(keyPath: KeyPath<MyDataModel, T>) -> [Element] {
        self.sorted { item1, item2 in
            return item1[keyPath: keyPath] < item2[keyPath: keyPath]
        }
    }
}

 

let newArray = array.customSorted(keyPath: \.title)
let newArray = array.customSorted(keyPath: \.count)
let newArray = array.customSorted(keyPath: \.date)

 

여기서 좀 더 유연하게 코드를 만든다면  MyDataModel 뿐만 아니라 어떤 타입이던 customSorted 를 사용할 수 있게 할 수 있습니다.

코드를 설명

  • T이름으로 제너릭 타입을 선언합니다.
  • 값을 비교해서 정렬해야하므로 Comparable 프로토콜을 준수하는 T타입이여야 합니다.
  • 인자값으로 외부 인자값 이름으로는 '_'으로 생략하였고 내부 인자값 이름으로는 keyPath로 만들어 사용합니다.
  • keyPath<Element, T> 으로 keyPath를 받아서 해당 인스턴스의 값을 keyPath로 접근하여 값을 비교합니다.
//MARK: T 모든 타입 사용 가능
extension Array {

    func sortedByKeyPath<T: Comparable>(_ keyPath: KeyPath<Element, T>, ascending: Bool = true) -> [Element] {
        self.sorted { item1, item2 in
            let value1 = item1[keyPath: keyPath]
            let value2 = item2[keyPath: keyPath]
            
            return ascending ? (value1 < value2) : (value1 > value2)
        }
    }
}

 

또는 아래처럼 만들어서 sorted처럼 값을 반환하지 않고 sort로 값을 바로 바꾸는 방법도 있게습니다.

extension Array {
    mutating func sortByKeyPath<T: Comparable>(_ keyPath: KeyPath<Element, T>, ascending: Bool = true) {
        self.sort { item1, item2 in
            let value1 = item1[keyPath: keyPath]
            let value2 = item2[keyPath: keyPath]
            
            return ascending ? (value1 < value2) : (value1 > value2)
        }
    }
}

 

전체 코드

import SwiftUI

struct MyDataModel: Identifiable {
    let id = UUID().uuidString
    let title: String
    let count: Int
    let date: Date
}

//MARK: MyDataModel 한정 사용 가능
//extension Array where Element == MyDataModel {
//    func customSorted<T: Comparable>(keyPath: KeyPath<MyDataModel, T>) -> [Element] {
//        self.sorted { item1, item2 in
//            return item1[keyPath: keyPath] < item2[keyPath: keyPath]
//        }
//    }
//}

//MARK: T 모든 타입 사용 가능
extension Array {
    
    mutating func sortByKeyPath<T: Comparable>(_ keyPath: KeyPath<Element, T>, ascending: Bool = true) {
        self.sort { item1, item2 in
            let value1 = item1[keyPath: keyPath]
            let value2 = item2[keyPath: keyPath]
            
            return ascending ? (value1 < value2) : (value1 > value2)
        }
    }
    
    func sortedByKeyPath<T: Comparable>(_ keyPath: KeyPath<Element, T>, ascending: Bool = true) -> [Element] {
        self.sorted { item1, item2 in
            let value1 = item1[keyPath: keyPath]
            let value2 = item2[keyPath: keyPath]
            
            return ascending ? (value1 < value2) : (value1 > value2)
        }
    }
}

struct ContentView: View {
    
    @State private var dataArray: [MyDataModel] = []
    
    var body: some View {
        VStack {
            List {
                ForEach(dataArray) { item in
                    VStack(alignment: .leading) {
                        Text(item.id)
                        Text(item.title)
                        Text("\(item.count)")
                        Text(item.date.description)
                    }
                    .font(.headline)
                }
            }
            .onAppear {
                var array = [
                    MyDataModel(title: "Three", count: 3, date: .distantFuture),
                    MyDataModel(title: "One", count: 1, date: .now),
                    MyDataModel(title: "Two", count: 2, date: .distantPast)
                ]
                
                // let newArray = array.sortedByKeyPath(\.title)
                // dataArray = newArray
                array.sortByKeyPath(\.count)
                dataArray = array
            }
        }
    }
}

#Preview {
    ContentView()
}

 

 

 

 

 

 

'SwiftUI' 카테고리의 다른 글

List  (0) 2023.10.30
Navigation Stack  (0) 2023.10.26
Computed Properties  (0) 2023.10.26
projectedValue  (0) 2023.10.25
Binding  (0) 2023.10.25
ytw_developer