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)")
}
}
}
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.