SwiftUI’s PreferenceKey declaration is as following:

public protocol PreferenceKey {
  associatedtype Value
  static var defaultValue: Self.Value { get }
  static func reduce(value: inout Self.Value, nextValue: () -> Self.Value)
}

While it’s clear what both Value and defaultValue are and do, the same cannot be said for reduce(value:nextValue:):
in this article let’s take a deep dive into this mysterious method.

Official definition

Here’s the current SwiftUI’s headers for reduce:

/// Combines a sequence of values by modifying the previously-accumulated
/// value with the result of a closure that provides the next value.
///
/// This method receives its values in view-tree order. Conceptually, this
/// combines the preference value from one tree with that of its next
/// sibling.
///
/// - Parameters:
///   - value: The value accumulated through previous calls to this method.
///     The implementation should modify this value.
///   - nextValue: A closure that returns the next value in the sequence.
static func reduce(value: inout Self.Value, nextValue: () -> Self.Value)

This definition sets some foundation on what’s the core functionality of reduce:
it’s used to compute a view preference key value, only when multiple children modify that key.

Let’s make an example.

NumericPreferenceKey

The following is a simple preference definition that holds an integer as its value:

struct NumericPreferenceKey: PreferenceKey {
  static var defaultValue: Int = 0
  static func reduce(value: inout Int, nextValue: () -> Int) { ... }
}

From now on every view in any view hierarchy has a default value of 0 for NumericPreferenceKey, regardless of the reduce implementation.

When is reduce invoked

Imagine a small view hierarchy with one root, two leaves, and nothing in between:

VStack {
  Text("A")
  Text("B")
}

For clarity’s sake: VStack is the root, while the two Texts are the leaves.

We will use this hierarchy in different scenarios.

No child alters/sets the preference key

VStack {
  Text("A")
  Text("B")
}

Here no view sets its own NumericPreferenceKey value, therefore all views have a NumericPreferenceKey value of NumericPreferenceKey.defaultValue, which is 0 as per our definition.

NumericPreferenceKey.reduce will never be called on the Texts, as no one can pass a value to a leaf.

reduce is also not called on VStack, because its children don’t set/pass a NumericPreferenceKey value to their parent.

One child alters/sets the preference key

VStack {
  Text("A")
    .preference(key: NumericPreferenceKey.self, value: 1)
  Text("B")
}

In this case:

  • Text("A") sets its NumericPreferenceKey value to 1 and pass it to its parent
  • Text("B") defaults NumericPreferenceKey to defaultValue, and not pass anything to its parent

What about VStack? Let’s take a look at the reduce definition once again: Combines a sequence of values by modifying the previously-accumulated value with the result of a closure that provides the next value.

Since only children that have set/changed the NumericPreferenceKey value will pass it to their parents, VStack will only have accumulated one value: 1 from Text("A").

Therefore, once again, NumericPreferenceKey.reduce is also not called on VStack, and the NumericPreferenceKey value associated to VStack is now 1.

Multiple children alter/set the preference key

VStack {
  Text("A")
    .preference(key: NumericPreferenceKey.self, value: 1)
  Text("B")
    .preference(key: NumericPreferenceKey.self, value: 3)
}

In this example:

  • both Texts set and pass to their parent a NumericPreferenceKey value of 1 and 3 respectively
  • VStack accumulates two NumericPreferenceKey values

SwiftUI doesn’t know what NumericPreferenceKey value to assign to VStack, as multiple values are proposed from its children:
this is where our NumericPreferenceKey.reduce comes to the rescue, helping SwiftUI reduce these multiple values into one, which will be then assigned to our VStack.

NumericPreferenceKey.reduce would be called even if all passed values were the same.

So what’s the value of VStack? Before answering this, we need to know in what order the values are passed to VStack.

Reduce call order

PreferenceKey’s reduce method always contains two parameters: the current value, and the next value to merge.

Going back to our example:

  1. VStack first receives the value 1 from Text("A"). As no other value was previously accumulated, this becomes the current value of VStack
  2. then VStack receives the value 3 from Text("B"), now SwiftUI needs to combine this value with the current value, therefore calling NumericPreferenceKey.reduce with 1 as the value parameter, and 3 as the nextValue

This is what the SwiftUI header meant by This method receives its values in view-tree order.: reduce is always called by traversing our view’s children from first to last, in declaration order.

If our VStack had Texts from "A" to "Z", all setting their NumericPreferenceKey value, reduce would be called first with the current value, inherited from Text("A"), and Text("B"), then with the new current value and Text("C"), etc.

reduce is called only between values accumulated within siblings: if a VStack child had its own children, the same concepts would be applied recursively, and then that child would pass to VStack its final value, regardless of how it was obtained.

It’s finally time to compute our VStack’s NumericPreferenceKey value:
to do so, we need to take a look at the NumericPreferenceKey.reduce implementation.

Common reduce implementations

Each preference key declaration has its own reduce implementation:
in this section, let’s cover some of the most common ones.

value = nextValue()

The most common definition assigns nextValue() to value, this could also be NumericPreferenceKey implementation:

struct NumericPreferenceKey: PreferenceKey {
  static var defaultValue: Int = 0

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

Let’s go back to our example where both Text("A") and Text("B") pass a value, and compute VStack’s NumericPreferenceKey:

  • first VStack takes in the value passed by Text("A"), as there was no prior accumulated value, this is the new VStack current value
  • then VStack gets the value passed by Text("B"), as we have two values reduce is called, and the new VStack value will be whatever the new proposed value is (that’s what value = nextValue() does).

In other words, with this implementation, when multiple children pass a value, reduce will discard all of them but the last one, which will become the value of our view.

Empty implementation

In previous articles we’ve defined various preference keys with an empty reduce implementation:

struct NumericPreferenceKey: PreferenceKey {
  static var defaultValue: Int = 0

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

Once again, let’s go back to our example and compute VStack’s NumericPreferenceKey:

  • first VStack takes in the value passed by Text("A"), as there was no prior accumulated value, this is the new VStack current value
  • then VStack gets the value passed by Text("B"), as we have two values reduce is called, and nothing happens, as our reduce does nothing. VStack keeps the current value.

This implementation is the opposite of the previous one: our view will keep the very first collected value, and ignore the rest.

value += nextValue()

Other common implementations use reduce to combine all values with some math operators such as sum:

struct NumericPreferenceKey: PreferenceKey {
  static var defaultValue: Int = 0

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

It should be intuitive by now that, in this case, our view will have as its value the sum of all the values passed by its children.

And many more

Other implementations worth mentioning are on preference keys whose Value is either an array or a dictionary, and where the reduce method is used to group all the children values together (via append(contentsOf:) or similar).

Once we understand the inner workings of preference key, it becomes intuitive to read and understand the effects of reduce.

PreferenceKey is a function of the current state

Like SwiftUI views, preference key values are the outcome of the current state and are not persisted.

If we look at the value += nextValue() reduce implementation for example, the current view value is the sum of the current passed values: if one children changes the passed value, SwiftUI will re-compute our view preference key value from scratch.

The same is true for any preference key Value: even in case of arrays or dictionaries. We always start over, nothing is persisted.

When is the preference key computed?

If the complete view in our app is our VStack example, reduce is actually never called:

struct ContentView: View {
  var body: some View {
    VStack {
      Text("A")
        .preference(key: NumericPreferenceKey.self, value: 1)
      Text("B")
        .preference(key: NumericPreferenceKey.self, value: 3)
    }
  }
}

This is true despite VStack having multiple NumericPreferenceKey values passed: did this article lied to us?

SwiftUI always strive to do as little as possible to present the final outcome to the end user, in this example no one is reading nor utilizing the preference key: therefore SwiftUI will ignore it.

All our keys are actually there and are present in their right place in the view hierarchy, they’re just not used, therefore SwiftUI won’t spend any time on resolving them.

If we want to see reduce getting called, we need to use NumericPreferenceKey somehow, one way is to add an onPreferenceChange(_:perform:) function in our VStack:

struct ContentView: View {
  var body: some View {
    VStack {
      Text("A")
        .preference(key: NumericPreferenceKey.self, value: 1)
      Text("B")
        .preference(key: NumericPreferenceKey.self, value: 3)
    }
    .onPreferenceChange(NumericPreferenceKey.self) { value in
      print("VStack's NumericPreferenceKey value is now: \(value)")
    }
  }
}

onPreferenceChange(_:perform:) tells SwiftUI that we’re interested in knowing what our VStack NumericPreferenceKey value is and when it changes: this is all we need to see our reduce method getting called.

Why is reduce’s nextValue a function?

Something that probably comes out as perplexing when reading PreferenceKey’s definition is why the reduce arguments are a value and a function: we’re combining two values, right? Why can’t SwiftUI just give us the explicit next value already?

public protocol PreferenceKey {
  associatedtype Value
  static var defaultValue: Self.Value { get }
  static func reduce(value: inout Self.Value, nextValue: () -> Self.Value)
}

It turns out that the reason why is once again SwiftUI laziness.

Let’s take our previous reduce empty implementation and use it in a slightly more complicated example:

struct ContentView: View {
  var body: some View {
    VStack {
      Text("A")
        .preference(key: NumericPreferenceKey.self, value: 1)

      VStack {
        Text("X")
          .preference(key: NumericPreferenceKey.self, value: 5)
        Text("Y")
          .preference(key: NumericPreferenceKey.self, value: 6)
      }
    }.onPreferenceChange(NumericPreferenceKey.self) { value in
      print("VStack's NumericPreferenceKey value is now: \(value)")
    }
  }
}

struct NumericPreferenceKey: PreferenceKey {
  static var defaultValue: Int = 0
  static func reduce(value: inout Int, nextValue: () -> Int) { 
  }
}

Here we have a VStack as our root, this VStack contains two children: a Text("A") and another VStack, which, in turn, has two Texts as children.

All Texts in the view set their own NumericPreferenceKey, and we call onPreferenceChange(_:perform:) on our root.

Let’s compute the root NumericPreferenceKey value:

  • first VStack receives the value passed by Text("A"), as there was no prior accumulated value, this is the new VStack current value
  • then it receives another value from its other child, the inner VStack, and our reduce method gets called

In this example reduce does nothing, we don’t need to know what the exact value passed by our inner VStack is.

Since we do not access to nextValue, SwiftUI won’t even compute it.

This means that the inner VStack preference key is not computed at all, as no one reads it, therefore our reduce is called just once, to resolve the root VStack preference key only.

And this is why reduce takes in a value and a method: the nextValue() method is a way for SwiftUI to check if that value is actually needed, and if it’s not, it won’t resolve it.

SwiftUI needs to resolve the whole view hierarchy as quickly and as efficiently as possible, this is yet another optimization.

Conclusions

SwiftUI’s PreferenceKey is one of those behind the scenes tools that are not very popular, but yet are indispensable to obtain certain results:
in this article we explored PreferenceKey’s inner workings and revealed how its reduce method is used and what it is for, discovering even more SwiftUI effectiveness.

All we’ve seen today is very much undocumented: please let me know if I’ve missed anything!

As always, thank you for reading and stay tuned for more SwiftUI articles!

⭑⭑⭑⭑⭑