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 |