SwiftUI views layout depends on each view state. This state is composed of a mix of internal properties, external values coming from the environment, etc.

When it comes to advanced custom layouts, sometimes a view needs information from its children, direct or not, as well.

A common example is when ancestors need to know their children size:
in this article let’s explore how can do so.

The view size is an example: the same concept applies to any other type conforming to Equatable.

Reading a view size

When we need space information, we basically have one option in SwiftUI: GeometryReader.

GeometryReader is a view that fills all the available space, both horizontally and vertically, and comes with a GeometryProxy instance, which gives us access to the size and coordinate space of its container.

var body: some View {
  GeometryReader { geometryProxy in
    ...
    // Use geometryProxy to get space information here.
  }
}

In our case we don’t want to use GeometryReader directly: instead, we’re interested in the space information of specific views.

SwiftUI provides .overlay() and .background(), which, respectively, add an extra view in front and behind another view. Most importantly, the proposed size for these views is equal to the size of the view they’re applied to, making them a perfect candidate for what we are looking for:

var body: some View {
  childView
    .background(
      GeometryReader { geometryProxy in
        ...
        // Use geometryProxy to get childView space information here.
      }
    )
}

GeometryReader still requires us to declare a view within its body, we can use Color.clear to create an invisible layer:

var body: some View {
  childView
    .background(
      GeometryReader { geometryProxy in
        Color.clear
        // Use geometryProxy to get childView space information here.
      }
    )
}

Great! Now we have our space information, it’s time for our children to learn how to communicate to their ancestors.

Child to ancestors communication

SwiftUI gives us the power of PreferenceKeys, which is SwiftUI’s way to pass information through the view tree.

Let’s start by defining our own PreferenceKey, SizePreferenceKey:

struct SizePreferenceKey: PreferenceKey {
  static var defaultValue: CGSize = .zero
  static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}

PreferenceKey is a generic protocol that requires one static function and one static default value:

  • defaultValue is the value used when a view has no explicit value for this key
  • reduce(value:nextValue:) combines the key values found in the tree with a new one

We will use PreferenceKey to store the measured size of our child, going back to our example:

var body: some View {
  childView
    .background(
      GeometryReader { geometryProxy in
        Color.clear
          .preference(key: SizePreferenceKey.self, value: geometryProxy.size)
      }
    )
}

Now the child size is in the tree hierarchy! How can we read it?

SwiftUI provides a View extension, onPreferenceChange(_:perform:), which lets us specify the key we’re interested in, and a code block to execute when that preference changes:

var body: some View {
  childView
    .background(
      GeometryReader { geometryProxy in
        Color.clear
          .preference(key: SizePreferenceKey.self, value: geometryProxy.size)
      }
    )
    .onPreferenceChange(SizePreferenceKey.self) { newSize in
      print("The new child size is: \(newSize)")
    }
}

Thanks to onPreferenceChange any ancestor interested in this key can extract and get notified when the value changes.

Extension

This way to obtain a child size is so handy that I find myself using it multiple times, instead of copy-pasting it over and over, I’ve written a View extension for it:

extension View {
  func readSize(onChange: @escaping (CGSize) -> Void) -> some View {
    background(
      GeometryReader { geometryProxy in
        Color.clear
          .preference(key: SizePreferenceKey.self, value: geometryProxy.size)
      }
    )
    .onPreferenceChange(SizePreferenceKey.self, perform: onChange)
  }
}

This extension takes in a function to be called whenever the view size is updated. Going back to our example, our new body declaration is:

var body: some View {
  childView
    .readSize { newSize in
      print("The new child size is: \(newSize)")
    }
}

Much better. The plug and play gist can be found here.

Conclusions

Once an ancestor has access to the key value, it’s really up to us to decide what do with it:
we can use it for example to force multiple elements to share the same value (aka the same size in the example above), and much, much more.

In following articles we will see some examples: in the meanwhile, do you use PreferenceKey? have you seen any cool example? Please let me know!

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

⭑⭑⭑⭑⭑