What is a PreferenceKey, and how do values travel from children up to ancestors?

8.8.1 · L5 · Interview

Predict first

Two children publish 30 and 20 to a key whose reduce keeps the maximum. What is the combined value?

import SwiftUI

struct MaxKey: PreferenceKey {
    static let defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = max(value, nextValue()) }
}
var v = MaxKey.defaultValue
MaxKey.reduce(value: &v, nextValue: { 30 })
MaxKey.reduce(value: &v, nextValue: { 20 })
print(v)
  • 20.0
  • 30.0
  • 50.0

Mental model. Preferences are children raising their hands. The reduce function is the teacher’s rule for summarising all the raised hands into one answer.

A PreferenceKey is the upward counterpart of the environment. A child publishes a value with .preference(key: MyKey.self, value: x), and an ancestor reads the combined result with .onPreferenceChange(MyKey.self) { … }, or with .overlayPreferenceValue and .backgroundPreferenceValue to build views from it. Many children may publish the same key, so you define a reduce function that combines them: reduce(value: inout Value, nextValue: () -> Value), applied in tree order starting from defaultValue. A “max width” key takes the larger of the two values; a “list of frames” key appends. Because the combination is defined by you, the result is deterministic whatever the number of children.

It is how built-ins like navigationTitle and tab item labels communicate upward, and how you implement measure-then-use patterns: a child reports its size through a preference (read with a GeometryReader in its background), the parent stores it in @State and lays out other things to match (equal widths, a scroll indicator, an anchored popover). One caution: writing state in onPreferenceChange triggers another update, so guard against loops (only set it when the value really changes). For simple one-off measurement, newer onGeometryChange is often easier, but preferences remain the general mechanism, and making the key’s value Equatable helps SwiftUI skip needless updates.

Each published value is folded into the running value by reduce, here keeping the larger one.

Common traps

  • Setting state in onPreferenceChange unconditionally. It can loop, as the state change triggers a new layout and a new preference.
  • Writing a reduce that depends on child order. Use an order-independent combination unless order is the point.

They might follow up with

  • How would you make several buttons the same width? Each reports its width via a preference; the parent keeps the maximum and applies it.
  • What is defaultValue for? The starting value for reduction, and what is returned when no child publishes.

Related questions

  • 8.8.2 What are the coordinate spaces in SwiftUI, and how do you read a view’s frame?
  • 8.3.2 How does the SwiftUI environment work, and when should you use it instead of passing a parameter?
  • 8.5.2 When do you use GeometryReader, and how do safe areas work in SwiftUI?

What are the coordinate spaces in SwiftUI, and how do you read a… →