본문 바로가기

SwiftUI

SwiftUI) 하위 뷰의 크기 추적(Preference)

문제 상황

특정 뷰에 대한 크기를 파악해야 하는데 특정 뷰가 서버 값을 통해서 중간에 크기가 달라지는 경우가 발생하였다.

기존에 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)
    }
}

 

 

이를 통해서 뷰의 크기가 변화하는 것을 관찰할 수 있다.