Preference values pass data up the view hierarchy, similar to how environment values pass data down the view hierarchy.
Preference values can be set using preference, or modified using transformPreference. These values can be read by onPreferenceChange, or backgroundPreferenceValue or overlayPreferenceValue if you want to add a background/overlay based on a preference value.
Here is a simple example to demonstrate that preference values can be read from the parent view:
struct SomeKey: PreferenceKey {
static let defaultValue: Int = 0
static func reduce(value: inout Int, nextValue: () -> Int) {
value += nextValue()
}
}
struct Parent<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
content
.overlayPreferenceValue(SomeKey.self) { x in
Text("\(x)")
}
}
}
#Preview {
Parent {
VStack {
Color.red
.preference(key: SomeKey.self, value: 2)
Color.green
.preference(key: SomeKey.self, value: 3)
}
}
}
Notice the reduce method in the preference key. This method is used to combine preference values of sibling views, since the parent must receive one value even if its subviews have different preference values set. In the above example, the overlay shows "5".
An important invariant that the implementation must satisfy is that the defaultValue must be an identity of reduce - reduce(value: &x, nextValue: {defaultValue}) should not change x.
navigationTitle uses transformPreference to set some preference value representing some tool bar configuration. You can peek into the internals of SwiftUI by printing
print(type(of: Text("").navigationTitle("")))
This prints:
ModifiedContent<ModifiedContent<Text, TransactionalPreferenceTransformModifier<NavigationTitleKey>>, _PreferenceTransformModifier<ToolbarKey>>
_PreferenceTransformModifier is the view modifier that transformPreference applies, and in this case it is transforming some preference key called ToolbarKey.
toolbar and presentationDetents are more opaque in how they work, and might not be implemented using a preference value.
There is also ContainerValues. Unlike preference values which sends data up the view hierarchy as far as possible (similar to how environment values are sent as far down as possible), container values is only sent up one level, and the parent can read each individual subview's container values. This is similar to how tag works.
For completeness, here is a demonstration:
extension ContainerValues {
@Entry var someValue = 0
}
struct Parent<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
VStack {
ForEach(subviews: content) { subview in
subview
.overlay {
Text("\(subview.containerValues.someValue)")
}
}
}
}
}
#Preview {
Parent {
Color.red
.containerValue(\.someValue, 2)
Color.green
.containerValue(\.someValue, 3)
}
}