How to read a view size in SwiftUI

SwiftUI
12 August 2020

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

When it comes to advanced custom layouts, sometimes a view might need layout information from its children.

A typical example is when ancestors need to know their children's size: 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 have one option in SwiftUI: the GeometryReader.

GeometryReader is a view that fills all the available space in all directions and comes with a GeometryProxy instance, which gives us access to its container's size and coordinate space.

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 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! We now have our space information: it's time for our children to learn to communicate with their ancestors.

Child to ancestors communication

SwiftUI gives us the power of PreferenceKeys, which is SwiftUI's way to pass information "up" on the view tree.

Let's start by defining our 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

Refer to PreferenceKey's reduce method demystified for a deeper look into the reduce(value:nextValue:) method.

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 let us pass a closure to be executed 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 our values change.

Extension

This way to obtain a child size is so handy that I find myself using it all the times, instead of copy-pasting it over and over, we can write 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 closure 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 to do with it:
we can use it, for example, to force multiple elements to share the same value (e.g., the same size) and much, much more.

We will use this technique in many articles to come: do you use PreferenceKey? have you seen any cool examples? Please let me know!

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

⭑⭑⭑⭑⭑

Further Reading

Explore SwiftUI

Browse all