티스토리 뷰

반응형

KeyPath 를 SwiftUI 쪽에서 자주 쓰면서

Swift 코드에도 자주 사용하고 싶어서 정리 및 useful example 을 모아두려고 한다.

 

사실.. 예를들어 map(\.xx) 이런 코드  많이 축약됐네~ 이런 느낌도 잘안들고 익숙하지도 않아서 잘안썼는데

앞으로는 의도적으로 더 많이 써보고 싶음.


[참고자료]

- Swift Docs > Key-Path Expressions  

- KeyPath 구현  

 


[1] Key Path 정리 

 

1. 백슬래쉬를 쓰는 이런 Key-Path Expression 은 컴파일 타임에 KeyPath class 의 instance 로 교체된다. 

\<#type name#>.<#path#>

 

2. 모든 타입에 subscript(keyPath:) 가 구현되어있다. 이 서브스크립트에 keypath 를 넘기며 사용하면 된다.

struct SomeStructure {
    var someValue: Int
}


let s = SomeStructure(someValue: 12)
let pathToProperty = \SomeStructure.someValue


let value = s[keyPath: pathToProperty]
// value is 12

 

 

3. type name 은 context 에 따라 생략될 수 도 있다.

(예를들어 \SomeClass.someProperty 대신  \.someProperty 를 쓸 수 있다.) 

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
    // ...
}

 

4. path 는 self 를 참조할 수 있다.  \.self  이렇게 쓰는 keypath 를 identity keypath 라고 한다. 

identity key 를 통해  whole instance 를 참조할 수 있으며 데이터를 바꿀 수도 있다. 

var compoundValue = (a: 1, b: 2)
// Equivalent to compoundValue = (a: 10, b: 20)
compoundValue[keyPath: \.self] = (a: 10, b: 20)

 

5. path 는 괄호 (bracket) 을 사용한 subscript 를 포함할 수 있다. 단 subript 에 넘기는 타입이 Hashable 인 경우에만.

let greetings = ["hello", "hola", "bonjour", "안녕"]
let myGreeting = greetings[keyPath: \[String].[1]]
// myGreeting is 'hola'
var index = 2
let path = \[String].[index]
print(greetings[keyPath: path])

 

6.  path 는 optional chaining 과 forced unwrapping 을 사용할 수 있다.  아래는 keypath 에 optional chaining  을 사용한 예제.

let greetings = ["hello", "hola", "bonjour", "안녕"]

let firstGreeting: String? = greetings.first
print(firstGreeting?.count as Any)
// Prints "Optional(5)"


// Do the same thing using a key path.
let count = greetings[keyPath: \[String].first?.count]
print(count as Any)
// Prints "Optional(5)"

 

7.  function이나 closure 를 제공해야하는 context 에서 key path expression 을 사용할 수 있다.

특히 root type이 SomeType이고 path가 Value 타입인 경우, (SomeType) -> 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 }

 

 

8. KeyPath 는 static member 를 reference 할 수 없다. 

 

https://github.com/apple/swift/issues/57696

 

[SR-15374] Key paths cannot refer to static member · Issue #57696 · apple/swift

Previous ID SR-15374 Radar None Original Reporter @groue Type Improvement Additional Detail from JIRA Votes 30 Component/s Compiler Labels Improvement Assignee None Priority Medium md5: 58f9811cbdf...

github.com

 


 

[2] 유용한 사용예제

 

2.1  안전한 KVC 

objc 관련된 쪽에서  key-value coding 이나 key-value observing 을 쓸 때 안전하게 쓸 수 있다. 

예를들어 NSObject 의  value(forKey key: String) 를 쓸 때, 하드코딩 된 string 값을 keypath을 안넘기고 Key-Path String Expression 을 넘길 수 있다.
(#keyPath(SomeClass.someProperty) 를 print 해보면 "SomeClass.someProperty" 가 아니라
"someProperty"  로 출력되는 것을 확인할 수 있다. )

class SomeClass: NSObject {
    @objc let someProperty = "some.."
}

let c = SomeClass()

//if let value = c.value(forKey: "someProperty")) {
//    print(value)
//}

if let value = c.value(forKey: #keyPath(SomeClass.someProperty)) {
    print(value)
}

 

참고로 이렇게 생긴 폼을 key-path string expression 이라고 하며 

#keyPath(<#property name#>)

컴파일 타임에 key-path string expression은 string literal 로 대체된다.

 

-- 

또한 애플에서 제공하는 API 중 public setter 가 제공안되는 프로퍼티의 값을 바꿔야할 때, 

setValue(_:forKey:)  코드를 종종 쓰는데 이를 keyPath 를 통해 쓰면 더 좋을 것 이다. 

두가지 예제 첨부.

class CustomTabBarController: UITabBarController   {
    
    private func setupTabBar() {
        let tabBar = CustomTabBar()
        // self.setValue(tabBar, forKey: "tabBar")
        self.setValue(tabBar, forKey: #keyPath(UITabBarController.tabBar))
    }
 }

 

import StoreKit

extension SKProduct {
    convenience init(productIdentifier: String) {
        self.init()
        // self.setValue(productIdentifier, forKey: "productIdentifier")
        self.setValue(productIdentifier, forKey: #keyPath(SKProduct.productIdentifier))
    }
}

 

 

2.2  extension for generic 

 

The power of key paths in Swift 랑 비슷하게 예제를 구성함.

이렇게 다양한 타입에 sorted 를 써야하는 경우 

struct Song {
    let name: String
    let dateAdded: Date
    let ratings: Ratings
}

struct Ratings {
    let worldWide: Int
    let local: Int
}

let songs: [Song] = [
    Song(name: "aa", dateAdded: .now, ratings: Ratings(worldWide: 1, local: 1)),
    Song(name: "bb", dateAdded: .now, ratings: Ratings(worldWide: 1, local: 1)),
    Song(name: "cc", dateAdded: .now, ratings: Ratings(worldWide: 1, local: 1))
]

let s1 = songs.sorted(by: { $0.name < $1.name })
let s2 = songs.sorted(by: { $0.dateAdded < $1.dateAdded })
let s3 = songs.sorted(by: { $0.ratings.worldWide < $1.ratings.worldWide })

 

extension + keyPath 를 활용하면 축약이 가능하다

extension Array {

    func sorted<T: Comparable>(_ keyPath: KeyPath<Element, T>) -> [Element] {
        return self.sorted(by: { $0[keyPath: keyPath] < $1[keyPath: keyPath] })
    }
}

let s1 = songs.sorted(\.name)
let s2 = songs.sorted(\.dateAdded)
let s3 = songs.sorted(\.ratings.worldWide)

 

 

2.3 CellConfigurator

 

이 예제도 The power of key paths in Swift 에서 가져옴. 

공통으로 사용하는 TableViewCell 이 있다고 할때, 

이때 다음과 같이 CellConfiguartor 를 만들고 

struct CellConfigurator<Model> {
    let titleKeyPath: KeyPath<Model, String>
    let subtitleKeyPath: KeyPath<Model, String>
    let imageKeyPath: KeyPath<Model, UIImage?>

    func configure(_ cell: UITableViewCell, for model: Model) {
        cell.textLabel?.text = model[keyPath: titleKeyPath]
        cell.detailTextLabel?.text = model[keyPath: subtitleKeyPath]
        cell.imageView?.image = model[keyPath: imageKeyPath]
    }
}

 

각 타입에 맞게 cell 구성을 해줄 수 있을 것이다. 

let songCellConfigurator = CellConfigurator<Song>(
    titleKeyPath: \.name,
    subtitleKeyPath: \.artistName,
    imageKeyPath: \.albumArtwork
)
songCellConfigurator.configure(cell, for: song)


let playlistCellConfigurator = CellConfigurator<Playlist>(
    titleKeyPath: \.title,
    subtitleKeyPath: \.authorName,
    imageKeyPath: \.artwork
)
playlistCellConfigurator.configure(cell, for: playlist)

 

 

2.4 PropertWrapper 와 같이 쓰기

 

SwiftUI의 Environment property wrapper 에서  EnvironmentValues key path 를 활용하는 것 처럼

@Environment(\.locale) var locale: Locale
@Environment(\.colorScheme) var colorScheme: ColorScheme


/*
Checks the declared property’s wrappedValue. 

If the value changes, SwiftUI updates any parts of your view that depend on the value. 
For example, that might happen in the above example if the user changes the Appearance settings.
*/
if colorScheme == .dark { 
    DarkContent()
} else {
    LightContent()
}

 

Custom PropertyWrapepr 를 만들고 keypath 를 활용하는 방법도 잘 사용하면 유용할 것 이다.

구현은 Enviroment 쪽과 비슷하게 하면 될 것. 

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen @propertyWrapper public struct Environment<Value> : DynamicProperty {
    
    @inlinable public init(_ keyPath: KeyPath<EnvironmentValues, Value>)
    @inlinable public var wrappedValue: Value { get }
}

 

Enviroment 처럼 모아두기 좋은 예제로 AppSetting 이 떠올랐는데,

대충 이렇게 구현해서 사용할 수 있을 것이다.  (더 구체적인 내용은 Property Wapper  참고.)

import SwiftUI

@propertyWrapper
struct AppSetting<Value> {
    
    private let values = AppSettingValues()
    let wrappedValue: Value
    
    init(_ keyPath: KeyPath<AppSettingValues, Value>) {
        wrappedValue = values[keyPath: keyPath]
    }
}

struct AppSettingValues {
    
    var keyColor: Color {
        // dummy data
        return .orange
    }
    
    var isLabOn: Bool {
        // dummy data
        return true
    }
}

struct ContentView: View {
    
    @AppSetting(\.keyColor) var keyColor: Color
    @AppSetting(\.isLabOn) var isLabOn: Bool
    
    var body: some View {
        Text("Hello, world!")
            .foregroundColor(isLabOn ? keyColor : .black)
    }
}

 

프리뷰도 첨부.

 

 

 

 

 

반응형
댓글