티스토리 뷰
KeyPath 를 SwiftUI 쪽에서 자주 쓰면서
Swift 코드에도 자주 사용하고 싶어서 정리 및 useful example 을 모아두려고 한다.
사실.. 예를들어 map(\.xx) 이런 코드 많이 축약됐네~ 이런 느낌도 잘안들고 익숙하지도 않아서 잘안썼는데
앞으로는 의도적으로 더 많이 써보고 싶음.
[참고자료]
- Swift Docs > Key-Path Expressions
[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
[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)
}
}
프리뷰도 첨부.
'🍏 > Swift' 카테고리의 다른 글
[Swift] @TaskLocal (0) | 2024.08.23 |
---|---|
[Swift] @dynamicCallable 유용한 예제 모음 (4) | 2024.03.16 |
[Swift] RandomAccessCollection / BidirectionalCollection 의 Time Complexity (0) | 2023.09.23 |
[Swift] Continuations > completion or delegate 를 async 로 (0) | 2023.08.02 |
[Swift] self in a closure in a closure (0) | 2023.07.11 |
- Total
- Today
- Yesterday
- 장고 URL querystring
- Sketch 누끼
- github actions
- 플러터 얼럿
- flutter build mode
- METAL
- Flutter 로딩
- ipad multitasking
- cocoapod
- 플러터 싱글톤
- Dart Factory
- 구글 Geocoding API
- 장고 Custom Management Command
- Django FCM
- Django Heroku Scheduler
- Watch App for iOS App vs Watch App
- flutter deep link
- Flutter Clipboard
- drf custom error
- PencilKit
- Django Firebase Cloud Messaging
- flutter dynamic link
- flutter 앱 출시
- Flutter Spacer
- Python Type Hint
- DRF APIException
- Flutter getter setter
- SerializerMethodField
- Flutter Text Gradient
- ribs
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |