SwiftUI PreferenceKey for child-to-parent communication

Sofia Martinez Jan 2026
1 tab
import SwiftUI

// MARK: - Size Preference
struct SizePreferenceKey: PreferenceKey {
    static var defaultValue: CGSize = .zero

    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
        value = nextValue()
    }
}

struct MeasurableView<Content: View>: View {
    let content: Content
    @Binding var size: CGSize

    init(size: Binding<CGSize>, @ViewBuilder content: () -> Content) {
        self._size = size
        self.content = content()
    }

    var body: some View {
        content
            .background(
                GeometryReader { geometry in
                    Color.clear
                        .preference(key: SizePreferenceKey.self, value: geometry.size)
                }
            )
            .onPreferenceChange(SizePreferenceKey.self) { newSize in
                size = newSize
            }
    }
}

// MARK: - Scroll Offset Preference
struct ScrollOffsetPreferenceKey: PreferenceKey {
    static var defaultValue: CGFloat = 0

    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

struct TrackableScrollView<Content: View>: View {
    let content: Content
    @Binding var scrollOffset: CGFloat

    init(scrollOffset: Binding<CGFloat>, @ViewBuilder content: () -> Content) {
        self._scrollOffset = scrollOffset
        self.content = content()
    }

    var body: some View {
        ScrollView {
            GeometryReader { geometry in
                Color.clear
                    .preference(
                        key: ScrollOffsetPreferenceKey.self,
                        value: geometry.frame(in: .named("scrollView")).minY
                    )
            }
            .frame(height: 0)

            content
        }
        .coordinateSpace(name: "scrollView")
        .onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
            scrollOffset = offset
        }
    }
}

// MARK: - Multiple Values Collection
struct ViewHeightKey: PreferenceKey {
    static var defaultValue: [Int: CGFloat] = [:]

    static func reduce(value: inout [Int: CGFloat], nextValue: () -> [Int: CGFloat]) {
        value.merge(nextValue()) { $1 }
    }
}

struct AdaptiveStack<Content: View>: View {
    let content: Content
    @State private var heights: [Int: CGFloat] = [:]

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        content
            .onPreferenceChange(ViewHeightKey.self) { heights in
                self.heights = heights
            }
    }
}

// Usage example
struct DynamicHeightExample: View {
    @State private var headerSize: CGSize = .zero
    @State private var scrollOffset: CGFloat = 0

    var body: some View {
        VStack {
            MeasurableView(size: $headerSize) {
                Text("Dynamic Header")
                    .font(.largeTitle)
                    .padding()
                    .background(Color.blue)
            }

            Text("Header size: \(headerSize.width) x \(headerSize.height)")

            TrackableScrollView(scrollOffset: $scrollOffset) {
                ForEach(0..<50) { index in
                    Text("Row \(index)")
                        .padding()
                }
            }

            Text("Scroll offset: \(scrollOffset)")
        }
    }
}
1 file · swift Explain with highlit

PreferenceKey enables child views to pass data up to ancestors, complementing the typical parent-to-child flow. I define a custom preference key by conforming to PreferenceKey protocol with a defaultValue and reduce method that combines multiple values. Child views set preferences with .preference(key:value:) modifier. Parent views read them with .onPreferenceChange(). Common use cases include measuring child sizes, collecting scroll positions, and building custom layouts. For anchors, Anchor preferences pass geometry information. This pattern enables complex layouts like badges on tab bars or dynamic height containers. PreferenceKeys make SwiftUI layouts more flexible without breaking the declarative paradigm.