-
Preference (PreferenceKey)iOS 2024. 11. 24. 20:36
1. Preference 란
상위 뷰에서 하위 뷰로 전달하는 환경 변수(Environment)와 달리
하위 뷰에서 구성된 정보를 상위 뷰 (Container)로 전달할 때 사용합니다.애플 문서의 그림이 해당 내용을 잘 표현해 주고 있는 것 같습니다.
Environment 는 여러 하위 뷰로 동시에 전달되지만 (flows down)
Preference는 하나의 단일 값으로 상위 뷰에 전달 됩니다. 따라서 어떻게 합쳐질지 결정해야 합니다. (flowing up)2. 사용
2.1 Modifier
navigationTitle 처럼 이미 Preference를 이용해 만들어져 있는 많은 Modifier 들이 존재합니다.
navigationTitle를 사용하는 예시를 보면,
NavigationView 내부 컨텐츠인 VStack에 navigationTitle을 정해주면 ( 하위에서 )
NavigationView의 title에 표시가 됩니다. ( 상위 컨테이너로 )import SwiftUI struct ContentView: View { @State private var currentTitle: String = "Initial Title" var body: some View { NavigationView { VStack { Button("Change Title") { currentTitle = "Updated Title" // 타이틀 변경 } .padding() } .navigationTitle(currentTitle) // 동적 타이틀 } } }
2.2 onPreferenceChange(_:perform:)
특정 PreferenceKey를 직접 감지하고 싶다면 onPreferenceChange(_:perform:) 를 사용합니다.
구현을 보면 아래와 같습니다.
키값에 해당하는 특정 Value 하나만을 클로져로 전달 받아서 사용할 수 있습니다.
하위에서 상위 컨테이너로는 하나의 값 단일 값으로 전달해야 한다는 문서의 내용을 생각해보면 자연스러운 모습입니다.func onPreferenceChange<K>( _ key: K.Type = K.self, perform action: @escaping (K.Value) -> Void ) -> some View where K : PreferenceKey, K.Value : Equatable
실제 사용하는 예시를 보면 아래와 같이 하위 뷰에서 .preference로 설정한 값을 onPreferenceChange 로 감지합니다.
VStack { SomeView .preference(key: MyPreferenceKey.self, value: myValue) } .onPreferenceChange(MyPreferenceKey.self) { value in // myValue self.preferenceValue = value }
3. 구현 및 이해
위와 같이 사용하기 위해 구현을 직접 만드는 것도 간단합니다.
key는 타입 자체이고, Value만 정해주면 될 것 같네요.PreferenceKey protocol 을 보면 다음과 같습니다.
protocol PreferenceKey { associatedtype Value static var defaultValue: Self.Value { get } static func reduce(value: inout Self.Value, nextValue: () -> Self.Value) }
defaultValue는 하위 뷰에서 따로 해당 키 값을 설정하지 않았을 때 제공할 기본 값입니다.
reduce 는 여러 하위들에서 preference 값이 있을 때 어떻게 합칠지 정의하는 함수입니다.주의해야 할 점은, reduce는 한 계층에서 타고 올라온 값들을 합칠 때 쓰는 게 아니라,
바로 하위에 여러 값이 있을 때 이들을 합치는 것 입니다.정확한 이해를 위해 예제를 한번 만들어 보겠습니다.
간단하게 Int 값을 갖는 Preference를 하나 정의했습니다.
별다른 구현은 하지 않았습니다.struct MyPreferenceKey: PreferenceKey { static var defaultValue: Int = 0 static func reduce(value: inout Int, nextValue: () -> Int) { } }
Red - Orange - Yellow - Green 으로 이어지는 포함 관계를 갖는 간단한 View 입니다.
각 뷰에서 MyPreferenceKey 값을 감지하도록 modifier를 추가해서 값 변화를 살펴보겠습니다struct ContentView: View { @State private var red = 0 @State private var orange = 0 @State private var yellow = 0 @State private var green = 0 var body: some View { VStack { Text("Red \(red)") VStack { Text("Orange \(orange)") VStack { Text("Yellow \(yellow)") VStack { Text("Green \(green)") } .padding() .background(Color.green) .preference(key: MyPreferenceKey.self, value: 10) .onPreferenceChange(MyPreferenceKey.self) { value in self.green = value } } .padding() .background(Color.yellow) .onPreferenceChange(MyPreferenceKey.self) { value in self.yellow = value } } .padding() .background(Color.orange) .onPreferenceChange(MyPreferenceKey.self) { value in self.orange = value } } .padding() .background(Color.red) .onPreferenceChange(MyPreferenceKey.self) { value in self.red = value } } }
Preference reduce에서 아무런 작업도 해주지 않았지만
제일 하위의 Green 에서 10으로 설정한 값이 최상위 Red 까지 타고 올라가서 전달되는 모습을 볼 수 있습니다.하위에서 추가된 Preference 값은 별다른 작업이 없었다면 상위 뷰까지 전달됩니다.
하지만 계층 중간에 새로운 값이 들어가면 해당 값을 대체됩니다.Orange에 새로운 값 30을 추가해 보겠습니다.
VStack { Text("Red \(red)") VStack { Text("Orange \(orange)") ... } .padding() .background(Color.orange) .preference(key: MyPreferenceKey.self, value: 30) // Ornage에서 30 추가 .onPreferenceChange(MyPreferenceKey.self) { value in self.orange = value } }
Green에서 설정한 값 10이 Yellow까지 타고 올라가다가
Orange에서 새로 설정한 값 30으로 대체되는 걸 확인할 수 있습니다.3.1 reduce(value: nextValue:)
만약 이 MyPreferenceKey가 여러 번 불리면 어떻게 될까요?
참고로 현재는 MyPreference 의 reduce 에서는 아무 구현도 하지 않은 상태입니다.Orange Text가 있는 VStack과 같은 레벨의 Red Text에 새로운 preference value를 추가해 보겠습니다.
VStack { Text("Red \(red)") .preference(key: MyPreferenceKey.self, value: 10) // Orange 와 같은 레벨에서 100 추가 VStack { Text("Orange \(orange)") ... } .padding() .background(Color.orange) .preference(key: MyPreferenceKey.self, value: 30) // Ornage에서 30 추가 .onPreferenceChange(MyPreferenceKey.self) { value in self.orange = value } }
reduce에서 아무 구현도 해주지 않으면 새로 추가한 100으로 대체되는 걸 볼 수 있습니다.
참고로 Text를 아래에 쓰면 30이 됩니다.
VStack은 밑에서 부터 생성되어서 뒤에 불린 30으로 대체되는 것 같습니다.
(이 순서가 보장되는지는 확실하지 않음.)reduce 에서 새로운 값이 합쳐지도록 구현을 추가해 보겠습니다.
struct MyPreferenceKey: PreferenceKey { ... static func reduce(value: inout Int, nextValue: () -> Int) { value += nextValue() } }
그럼 위 구현대로 Red Text 에서 추가한 100과 Orange VStack에서 추가한 값 30이 합쳐지는 걸 확인할 수 있습니다.
4. 결론
- Preference 는 하위에서 어떤 값을 감지하기 위해 사용합니다.
- 하위 뷰에서 변경된 값은 최상위 뷰까지 전달 됩니다.
- 상위 컨테이너로 전달될 때는 하나의 단일 값으로만 전달 됩니다.
- 하위 여러 뷰에서 Preference 값이 동시에 여러 번 불릴 때 값을 원하는 결과로 만들려면 reduce에서 구현합니다.읽어주셔서 감사합니다 :)
'iOS' 카테고리의 다른 글
[iOS] OSSignpost > 특정 작업의 duration 측정하기 (0) 2024.07.01 이미지의 크기를 줄인다. 리사이징 ? 압축 ? 해상도? 품질? (0) 2024.05.10 pixel 과 points, dp의 관계 그리고 UIImage (0) 2024.05.10 SwiftLint SPM으로 설치하기 (using Swift Package Build Tool Plugin) (0) 2024.03.22 swift concurrency 사용해 이미지 권한 얻기 (0) 2023.11.29