문제 상황
특정 뷰에 대한 크기를 파악해야 하는데 특정 뷰가 서버 값을 통해서 중간에 크기가 달라지는 경우가 발생하였다.
기존에 onAppear로 해당 뷰의 크기를 알아내고자 했을 때, 값을 한 번 알아낼 수는 있지만 그 후 서버에서 받은 값으로 뷰가 채워지고 크기가 달라지는 것까지 파악하지 못하는 문제가 있었다.
하위 뷰에서 상위 뷰로 값을 전달하는 것에 대한 문제를 찾아보던 중 Preference라는 프로토콜을 알게 되었다.
Preference란?
하위 뷰에서 컨테이너(부모 뷰)로 설정 값을 전달하기 위해 사용되는 API Collection이다.
하지만 여러 하위뷰에서 하나의 컨테이너로 값을 보낸다면 충돌이 있을 수 있기 때문에 이를 어떻게 합칠지 정의를 내려야 한다.
"a single container needs to reconcile potentially conflicting preferences flowing up from its many subviews."
이를 정의할 수 있도록 밑에서 나오겠지만 reduce 메서드가 존재한다.
추가로) .navigationTitle("")은 View에서 상위 네비게이션의 타이틀을 정할 수 있는데 이 메서드 역시 Preference를 활용한 것이라 한다.
PreferenceKey란?
Preference를 활용하기 위해서는 PreferenceKey를 통해 내가 어떤 값을 관찰하고 싶은지 타입의 정의해야 한다.

구성
associatedtype Value
- 전달할 값의 타입을 정의
static var defaultValue: Self.Value
- value: 초기값
static func reduce(value: inout Self.Value, nextValue: () -> Self.Value)
- value: 지금 현재의 값
- nextValue: 새롭게 전달된 값, 새로운 하위 뷰에서 보내온 값
여기서 reduce가 여러 하위 뷰들이 하나의 컨테이너뷰로 값을 보내느 과정에서 충돌이 나지 않도록 어떻게 merge할지 정의하는 과정이라고 볼 수 있다.
preference(key:value:)
- 하위 뷰에서 특정 PreferenceKey에 값을 설정
- 부모 뷰에서 이 값을 받아 사용할 수 있도록 전달
onPreferenceChange(_:perform:)
- 값이 변경될 때 부모 뷰에서 이를 감지하여 특정 동작을 수행
- 값이 변경될 때마다 perform 클로저가 실행
// PreferenceKey 정의 (최대 높이 추적)
struct HeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue()) // 가장 큰 높이 유지
}
}
struct ContentView: View {
@State private var observedHeight: CGFloat = 0 // 감지된 높이
@State private var hasChanged: Bool = false // 크기 변경 감지 여부
var body: some View {
VStack {
Text("현재 높이: \(observedHeight)")
.foregroundColor(hasChanged ? .red : .black)
.bold()
ChildView()
.background(GeometryReader { proxy in
Color.clear
.preference(key: HeightPreferenceKey.self, value: proxy.size.height)
})
}
.onPreferenceChange(HeightPreferenceKey.self) { newHeight in
if newHeight != observedHeight { // 높이가 한 번이라도 변경되었을 때
hasChanged = true
observedHeight = newHeight
}
}
}
}
// 크기가 바뀌는 하위 뷰
struct ChildView: View {
@State private var isExpanded = false
var body: some View {
VStack {
Text("Child View")
.padding()
.background(Color.blue)
.cornerRadius(10)
Button("크기 변경") {
isExpanded.toggle()
}
}
.frame(height: isExpanded ? 200 : 100) // 크기가 변경됨
.animation(.easeInOut, value: isExpanded)
}
}

이를 통해서 뷰의 크기가 변화하는 것을 관찰할 수 있다.
'SwiftUI' 카테고리의 다른 글
| SwiftUI View 성능 향상을 위한 종속성 최소화하기 (0) | 2023.12.18 |
|---|---|
| SwiftUI) GridItem 알아보기 (0) | 2022.11.27 |
| SwiftUI) LazyH(V)Grid 알아보기 (0) | 2022.11.24 |
| SwiftUI) Grid, GridRow 알아보기 (0) | 2022.11.22 |
| SwiftUI @Binding 알아보기 (0) | 2022.10.23 |