Skip to Content
Course content

230: Practice Exercise: Building a Custom PreferenceKey for View Communication

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When I first started working with complex SwiftUI layouts, I fell into a trap that I see almost every developer hit: I treated PreferenceKey like it was just a fancy, decoupled version of @Binding. I thought, "If I want a child view to tell the parent something, and the parent to react, I'll just use a PreferenceKey to sync the state."

The Myth: PreferenceKeys are for Two-Way State Syncing

The misconception is that PreferenceKeys are a general-purpose communication channel. You might try to use one to pass a value from a child to a parent, and then immediately have the parent pass a modified version of that value back down to the child via a binding or environment object, all within the same layout cycle.

If you try this, you'll run into the dreaded "Modification of state during view update" warning, or worse, an infinite layout loop that freezes your app. Why? Because PreferenceKeys are designed specifically for the bottom-up flow of information. They aren't meant to "sync" state; they are meant to "report" layout or identity data from the children up to the ancestors after the view hierarchy has been proposed.

The Reality: One-Way Upward Signals

Think of a PreferenceKey as a courier. The child view puts a message in an envelope and sends it up the chain. The parent doesn't "reach down" to grab it; the parent simply listens for when that envelope arrives. The key distinction is that this happens during the layout process. The most common—and correct—use case is when a parent needs to know something about a child's geometry (like its size or position) that the parent couldn't possibly know beforehand.

Let's look at a real scenario: You're building a custom Tab Bar where you want a sliding highlight indicator to move behind the active tab. The parent container needs to know the exact width and X-offset of the selected child tab to animate the highlight bar correctly. The parent can't just "guess" the width because the text in the tabs might change based on localization.

// First, we define the key. 
// We use a struct and conform to PreferenceKey.
struct TabWidthKey: PreferenceKey {
    // The default value if no child provides one
    static var defaultValue: CGFloat = 0

    // This is the magic: how to merge multiple values from different children
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue() 
    }
}

// The child view "reports" its width
struct TabItem: View {
    let title: String
    
    var body: some View {
        Text(title)
            .background(GeometryReader { geometry in
                Color.clear.preference(key: TabWidthKey.self, value: geometry.size.width)
            })
    }
}

// The parent "listens" for the report
struct TabBar: View {
    @State private var selectedTabWidth: CGFloat = 0

    var body: some View {
        HStack {
            TabItem(title: "Home")
            TabItem(title: "Settings")
        }
        .onPreferenceChange(TabWidthKey.self) { width in
            // Now the parent knows the width and can update the UI
            self.selectedTabWidth = width
        }
    }
}

Notice what happened there. The TabItem didn't try to change a variable in the parent directly. It just declared, "My width is X," and the TabBar opted into listening for that specific key. I prefer this approach because it keeps the TabItem completely agnostic of who its parent is. It just shouts its width into the void, and whoever cares can listen.

A quick tip: Always be careful with what you put in reduce. If you have multiple children sending values for the same key, reduce determines how those values are combined. In my example, I just took the latest value, but if you were building a list of sizes, you'd probably use an array and append the nextValue to the existing collection.




📋 Practical Task

Exercise: Building a Child-Sized Header Highlight

Your task is to build a view where a "Highlight Bar" (a thin colored rectangle) automatically matches the width of a child text element. Since the text can change, the parent must rely on a PreferenceKey to determine the bar's width.

  • Create a PreferenceKey named HeaderWidthKey that stores a CGFloat.
  • Create a child view called HeaderTitleView that uses a GeometryReader inside a .background()` modifier to send its own width up through the HeaderWidthKey.
  • Create a parent view called CustomHeader that contains the HeaderTitleView and a Rectangle()` (the highlight bar) positioned below it.
  • Use .onPreferenceChange(HeaderWidthKey.self) in the parent to update a @State variable that controls the .frame(width:) of the highlight bar.

Goal: When you change the text in HeaderTitleView, the highlight bar should automatically snap to the new width of the text.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.