iOS/SwiftUI

PreferenceKey

Hamp 2025. 10. 24. 22:59
반응형

👋 들어가기 전

SwiftUI를 통해 클론코딩을 할 때, 많이 등장하는 개념인데, 이제는 슬슬 학습할 때가 된 것 같다.

 

현재하고 있는 플젝에서도 적극적으로 도입할 예정!


🔑 PreferenceKey

 

🧩 역할

A view with multiple children automatically combines its values for a given preference into a single value
visible to its ancestors.

 

하위뷰가 상위뷰로 전달하고 싶은 값(Preference)을 지정할 떄 사용하는 Key

 

보통 상위 -> 하위로는 많이 전달했지만, 하위 -> 상위로 전달하는 역전 형태는 PreferenceKey를 이용하면
깔끔하게 처리할 수 잇음

 

🎯 자주쓰이는 용도

  • 뷰 크기/위치 측정
  • 스크롤 위치 추적
  • 커스텀 레이아웃

 

🧱 구조

struct MyPreferenceKey: PreferenceKey {

    associatedtype Value
    static var defaultValue: Self.Value
    static func reduce(value: inout Self.Value, nextValue: () -> Self.Value)

}

 

  • defaultValue
    • 명시적인 초기 값이 없을 경우, 사용할 기본 값
  • reduce
    • 이전에 누적된값을 수정하여, 하나의 값으로 결합 하는 함수
    • view-tree 순서로 값을 받음 (아래에서 설명)
    • value
      • 이전 호출의 결과로 누적된 값
    • nextValue
      • 새로 전달 받은 값

 

📨 값 수신 받기

onPreferenceChange(_:perform:) 메서드를 이용

 

 @preconcurrency @inlinable nonisolated public func onPreferenceChange<K>(
     _ key: K.Type = K.self, 
    perform action: @escaping @Sendable (K.Value) -> Void
) -> some View where K : PreferenceKey, K.Value : Equatable

 

수신 받을 PreferenceKey를 넣어주고, closure로 갱신된 값을 받는다.

 

📤 값 송신 하기

preference(key:value:) 메서드 이용

nonisolated
func preference<K>(
    key: K.Type = K.self,
    value: K.Value
) -> some View where K : PreferenceKey

 

Key에 해당하는 value를 넣어줌, 약간 딕셔너리 같음

 

🤖 예제 코드

예제 코드를 통해, 위에서 설명한 view-tree 순으로 호출된다는 느낌을 알아보자.

 

1️⃣ Key 선언

문자열 배열을 계속 누적함, 먼저 호출들어오는 값이 누군지 추적하려고 만듬

// 1) PreferenceKey 정의 — 값은 문자열 배열
struct OrderPreferenceKey: PreferenceKey {
    static var defaultValue: [String] = []

    // reduce가 어떻게 호출되는지 콘솔로 확인할 수 있게 print 추가
    static func reduce(value: inout [String], nextValue: () -> [String]) {
        let next = nextValue()
        print("reduce called. accumulated: \(value) next: \(next)")
        // "view-tree order"를 그대로 보존하려면 append 순서 유지
        value.append(contentsOf: next)
    }
}

 

2️⃣ 하위 뷰, 송신자 선언

struct ChildLabel: View {
    let id: String
    var body: some View {
        Text("Child \(id)")
            .padding(8)
            .background(Color(white: 0.95))
            // preference로 자신의 식별자 전달
            .preference(key: OrderPreferenceKey.self, value: [id])
            .onAppear {
                print("onAppear child \(id)")
            }
    }
}

 

3️⃣ 부모뷰, 수신자 선언

struct PreferenceOrderDemoView: View {
    @State private var receivedOrder: [String] = []

    var body: some View {
        VStack(spacing: 20) {
            Text("Received order: \(receivedOrder.joined(separator: ", "))")
                .padding()
                .multilineTextAlignment(.center)

            // 구조 1: 평평한(Flat) 선언 순서 A, B, C
            VStack {
                Text("Flat order (A, B, C):")
                HStack {
                    ChildLabel(id: "A")
                    ChildLabel(id: "B")
                    ChildLabel(id: "C")
                }
            }
            .padding()

            // 구조 2: 중첩 구조 (D 안에 E, F) 그리고 G — depth-first 예상: E, F, D, G 혹은 D 먼저?
            VStack {
                Text("Nested order (D contains E & F, then G):")
                VStack {
                    HStack {
                        ChildLabel(id: "E")
                        ChildLabel(id: "F")
                    }
                    // D를 표현하기 위해 E/F와 함께 D도 preference를 넣어봄
                    ChildLabel(id: "D")
                }
                ChildLabel(id: "G")
            }
            .padding()
        }
        .onPreferenceChange(OrderPreferenceKey.self) { value in
            print("onPreferenceChange received: \(value)")
            receivedOrder = value
        }
        .padding()
    }
}

 

4️⃣ 구조 및 결과해석

 

구조는 위에서 아래로, 왼쪽에서 오른쪽 순이 먼저다.

트리의 in-order 방식으로 보면 될 것 같다.

 

출력도 한번 비교해보자.

reduce called. accumulated: ["A"] next: ["B"]
reduce called. accumulated: ["A", "B"] next: ["C"]
reduce called. accumulated: ["E"] next: ["F"]
reduce called. accumulated: ["E", "F"] next: ["D"]
reduce called. accumulated: ["E", "F", "D"] next: ["G"]
reduce called. accumulated: ["A", "B", "C"] next: ["E", "F", "D", "G"]
onPreferenceChange received: ["A", "B", "C", "E", "F", "D", "G"]
onAppear child G
onAppear child D
onAppear child F
onAppear child E
onAppear child C
onAppear child B
onAppear child A

출처

https://developer.apple.com/documentation/swiftui/preferencekey

 

PreferenceKey | Apple Developer Documentation

A named value produced by a view.

developer.apple.com

 

반응형