Home Understanding SwiftUI's GeometryReader
Post
Cancel

Understanding SwiftUI's GeometryReader

Key Point

GeometryReader allows us to size and position our SwiftUI views relative to their container views.

Intro

Introduced in June 2019 as a core component of the SwiftUI framework, GeometryReader provides a new way to size and position views based on the parent’s geometry.

GeometryReader is particularly useful when creating responsive layouts that adapt to different screen sizes and orientations. It acts as a container view that provides its children with information about its size and position within the parent view’s coordinate space. Now let us explore how to use GeometryReader to create dynamic and responsive layouts in SwiftUI.

Using GeometryReader

To use GeometryReader, you first need to wrap the view you want to position or size in a GeometryReader block. Inside the block, you can access the parent’s geometry using the geometry property. This property provides various information such as size, frame, and safeAreaInsets that can be used to position and size child views.

For example, to create a view that is always centered in its parent, you can use the following code:

1
2
3
4
5
GeometryReader { geometry in
    Text("Hello World")
        .position(x: geometry.size.width / 2, y: geometry.size.height / 2)
}

In this example, the Text view is centered in its parent by positioning it at the center of the parent’s geometry. The size property of the geometry is used to determine the center point of the parent.

Another useful and common application of GeometryReader is to use the frame property of the geometry to determine the size and position of child views based on the parent’s frame. For example, to create a view that takes up half of the parent’s width and height, you can use the following code:

1
2
3
4
GeometryReader { geometry in
    Rectangle()
        .frame(width: geometry.frame(in: .global).width / 2, height: geometry.frame(in: .global).height / 2)
}

In this example, the Rectangle view takes up half of the parent’s width and height by setting its frame based on the parent’s frame. The frame(in: .global) is used to make sure the frame is in the coordinate system of the global view.

You can also use the safeAreaInsets property of the geometry to position views inside the safe area of the parent. For example, to create a view that is always positioned 10 points away from the safe area of the parent, you can use the following code:

1
2
3
4
5
6
GeometryReader { geometry in
    Text("Hello World")
        .padding(.top, geometry.safeAreaInsets.top + 10)
        .padding(.leading, geometry.safeAreaInsets.leading + 10)
}

In this example, the Text view is positioned 10 points away from the top and leading edge of the safe area of the parent by adding the safeAreaInsets property to the padding.

CircularProgressBar Example

An impressive example of using GeometryReader, is using it to build a a circular progress bar in SwiftUI. To achieve this, we can use GeometryReader to determine the size of the parent view and then use that information to create a circular shape that represents the progress.

Here is an example of how to create a circular progress bar using GeometryReader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct CircularProgressBar: View {
    let progress: CGFloat

    var body: some View {
        GeometryReader { geometry in
            ZStack {
                Circle()
                    .stroke(lineWidth: 8)
                    .foregroundColor(.gray)
                Circle()
                    .trim(from: 0, to: progress)
                    .stroke(style: StrokeStyle(lineWidth: 8, lineCap: .round))
                    .foregroundColor(.blue)
                    .rotationEffect(Angle(degrees: -90))
                    .animation(.easeInOut)
            }
            .frame(width: geometry.size.width, height: geometry.size.width)
        }
    }
}

In this example, the CircularProgressBar struct is a view that takes in a progress value between 0 and 1. The view is wrapped in a GeometryReader block so that we can access the parent’s size. We use this size to create two Circle views. The first Circle view is a gray background circle, and the second Circle view is a blue progress circle.

The second Circle view is trimmed using the trim method, with the from and to parameters set to 0 and the progress value, respectively. This creates the effect of the blue progress circle filling up as the progress value increases. The rotationEffect method is used to rotate the progress circle so that it starts at the top of the parent view. Finally, we use the animation method to add an easing animation to the progress bar.

By wrapping the progress bar view in a GeometryReader block, we can ensure that the progress bar always takes up the same amount of space in the parent view, regardless of its size. This makes it easy to use the progress bar in different parts of the app and ensure that it looks consistent.

With this example, we can see how GeometryReader allows us to create dynamic and responsive layouts that adapt to different screen sizes, and how it can be used to create impressive user interface elements like a circular progress bar that are both functional and visually appealing.

Global, Local and Custom Coordinates

GeometryReader provides several ways to access the parent’s geometry, including global, local, and custom coordinates. The difference between them lies in the way they are used to specify the position and size of child views.

Global

Global coordinates refer to the coordinates of the view in the global coordinate system. This is sort of the SwiftUI equivalence of UIScreen.main.bounds in UIKit. These coordinates are relative to the screen, the top-left corner of the device’s screen and are independent of the position of the parent view. To access the parent’s geometry in global coordinates, you can use the frame(in: .global) property of the geometry.

Local

Local coordinates, on the other hand, refer to the coordinates of the view in the parent’s local coordinate system, meaning its size and position in it’s direct container, which in this case is the GeometryReader. These coordinates are relative to the top-left corner of the parent view and take into account any transformations applied to the parent view. To access the parent’s geometry in local coordinates, you can use the frame(in: .local) property of the geometry.

Custom

Custom coordinates allow you to specify a custom coordinate space for the child view. This can be useful when working with views that have been transformed in some way, such as being rotated or scaled. To access the parent’s geometry in custom coordinates, you can use the frame(in: .named(“whateverCustomNameYouGaveIt”)) property of the geometry, where “whateverCustomNameYouGaveIt” is the name of the custom coordinate space.

TLDR

If you forget everything from this article just remember that GeometryReader allows us to give our views sizes and position them relative to their containers.

The coordinate space you want to use will depend on what question you want to answer:

  • Want to know where this view is on the screen? Use the global space.
  • Want to know where this view is relative to its parent? Use the local space.
  • Want to know where this view is relative to some other view? Use a custom space.

The choice of which coordinates to use will depend on the specific requirements of your layout. Global coordinates can be useful when working with views that need to be positioned independently of the parent view, while local coordinates are useful when working with views that need to be positioned relative to the parent view. Custom coordinates can be useful when working with views that have been transformed in some way, and you need to take into account those transformations in positioning and sizing the child views.

Conclusion

“Although it’s usually best to let SwiftUI perform automatic layout using stacks, it’s also possible to give our views sizes relative to their containers using GeometryReader. For example, if you wanted two views to take up half the available width on the screen, this wouldn’t be possible using hard-coded values because we don’t know ahead of time what the screen width will be.” - Paul Hudson from his blog hackingwithswift.com

In conclusion, the GeometryReader is a powerful tool that can help you create dynamic and flexible layouts in SwiftUI. It provides information about the size and position of its parent view, which can be used to position and size child views. With the GeometryReader, you can create highly customizable and responsive user interfaces that adjust to the size and position of the parent view.

This post is licensed under CC BY 4.0 by the author.