GeometryReader Overview for Beginners

Thursday, 18 February 2021
Updated 2 years ago
This is a new layout, but it is still in BETA.
I will start sending email newsletters once in a while, so in order to get notified when I release new content, make sure to follow me on twitter @liquidcoder .

Apple says: "A container view that defines its content as a function of its own size and coordinate space. This view returns a flexible preferred size to its parent layout."

@frozen struct GeometryReader<Content> where Content : View

In simple term, GeometryReader is a container that will fill the available space, unless given a fixed frame, and will provide its children with the exact size (width and height) that it occupies as well as its coordinates space (X-Origin and Y-Origin).

// $$liquidcoderFilename-ContentView

import SwiftUI

struct ContentView: View {

    var body: some View {
        Text("Hello, World!")
    }

}

Consider the following example code, as you see the text is perfectly centred with its size matching its content.

https://res.cloudinary.com/liquidcoder/image/upload/v1613477482/Overview%20blog%20posts/Shared/ziohvs60zjkqxnenf4zl.png

Let's now surround the text with a GeometryReader container, and what happens.

// $$liquidcoderFilename-ContentView

import SwiftUI

struct ContentView: View {

    var body: some View {
        GeometryReader { geometry in
            Text("Hello, World!")
                .border(Color.blue)
        }.border(Color.red)
    }

}

https://res.cloudinary.com/liquidcoder/image/upload/v1613479170/Overview%20blog%20posts/GeometryReader/hp9cyrmilrr0gsfkt9z0.png

Oops! The Text is not centred anymore. This is because GeometryReader align its children's with its origin (x = 0, y=0).

The other thing to note is that the container does not completely fill the entire screen, the top and bottom spaces accommodate the safe areas, so technically those space are not part of the usable screen unless ignored.

// $$liquidcoderFilename-ContentView

import SwiftUI

struct ContentView: View {

    var body: some View {
        GeometryReader { geometry in
            Text("Hello, World!")
                .border(Color.blue)

            Text("Green")
                .border(Color.green)
        }.border(Color.red)
    }

}

No matter how many children you add inside, they will all be stacked on top of each other aligned with the parent's origin.

https://res.cloudinary.com/liquidcoder/image/upload/v1613481623/Overview%20blog%20posts/GeometryReader/fml7lyux8cz4pmjylquf.png

GeometryProxy

Apple says:"A proxy for access to the size and coordinate space (for anchor resolution) of the container view."

GeometryReader will give us an object of type GeometryProxy upon initialisation that contains its size and coordinate space.

// $$liquidcoderFilename-ContentView

import SwiftUI

struct ContentView: View {

    var body: some View {
        GeometryReader { geometry in
            // highlight-start
            VStack(alignment: .leading)  {
                VStack(alignment: .leading)  {
                    Text("Height : \(geometry.size.height)")
                        .padding()

                    Text("Width : \(geometry.size.width)")
                        .padding()
                }
            }
            // highlight-end
        }.border(Color.red)
    }

}

https://res.cloudinary.com/liquidcoder/image/upload/v1613483436/Overview%20blog%20posts/GeometryReader/ikjsr8kuxbjhna2o8gfa.png

Now, let's change the frame and see what happens.

// $$liquidcoderFilename-ContentView

import SwiftUI

struct ContentView: View {

    var body: some View {
        GeometryReader { geometry in
            VStack(alignment: .leading)  {
                VStack(alignment: .leading)  {
                    Text("Height : \(geometry.size.height)")
                        .padding()

                    Text("Width : \(geometry.size.width)")
                        .padding()
                }
            }

        }.border(Color.red)
        // highlight-start
        .frame(width: 300, height: 400)
        // highlight-end
    }

}

https://res.cloudinary.com/liquidcoder/image/upload/v1613484168/Overview%20blog%20posts/GeometryReader/jihkn2url4cwncy6wvs5.png

As you can see the height and width on the GeometryProxy have changed reflecting the same values we set using the frame modifier.

Next, let's take a look to GeometryProxy's frame .

// $$liquidcoderFilename-ContentView

import SwiftUI

struct ContentView: View {

    var body: some View {
        GeometryReader { geometry in
            // highlight-start
            VStack(alignment: .leading) {

                Text("Global origin X : \(geometry.frame(in: .global).origin.x)")
                    .padding()

                Text("Global origin Y : \(geometry.frame(in: .global).origin.y)")
                    .padding()

                Text("Local origin X : \(geometry.frame(in: .local).origin.x)")
                    .padding()

                Text("Local origin Y : \(geometry.frame(in: .local).origin.y)")
                    .padding()
            }

            // highlight-end
        }.border(Color.red)
    }

}

https://res.cloudinary.com/liquidcoder/image/upload/v1613485460/Overview%20blog%20posts/GeometryReader/tzbmafzttmbqqcxp1hbz.png

The frame function returns a CGRect type which contains a size and an origin.

Taking a look at the preview, you can see that the Global origin Y is different from the Local origin Y, the global origin is relative to the parent whereas the local one is relative to itself.

// $$liquidcoderFilename-ContentView

import SwiftUI

struct ContentView: View {

    var body: some View {
        GeometryReader { geometry in
            // highlight-start
            VStack(alignment: .leading) {
                Text("Safe area top : \(geometry.safeAreaInsets.top)")
                    .padding()

                Text("Safe area bottom : \(geometry.safeAreaInsets.bottom)")
                    .padding()

                Text("Safe area leading : \(geometry.safeAreaInsets.leading)")
                    .padding()

                Text("Safe area trailing : \(geometry.safeAreaInsets.trailing)")
                    .padding()
            }
            // highlight-end
        }.border(Color.red)
    }

}

https://res.cloudinary.com/liquidcoder/image/upload/v1613487612/Overview%20blog%20posts/GeometryReader/hv7pxrxpqvlfzhhx2nww.png

You can also get access to the safe area insets directly from the GeometryProxy object, and a you will get valid values if there's no fixed frame applied to the GeometryReader .

// $$liquidcoderFilename-ContentView

import SwiftUI

struct ContentView1: View {

    var body: some View {
        GeometryReader { geometry in
            // highlight-start
            VStack(alignment: .leading) {
                Text("Global MinX : \(geometry.frame(in: .global).minX)")
                    .padding()

                Text("Global MinY : \(geometry.frame(in: .global).minY)")
                    .padding()

                Text("Local MinX : \(geometry.frame(in: .local).minX)")
                    .padding()

                Text("Local MinY : \(geometry.frame(in: .local).minY)")
                    .padding()
            }
            // highlight-end
        }.border(Color.red)
    }

}

https://res.cloudinary.com/liquidcoder/image/upload/v1613489930/Overview%20blog%20posts/GeometryReader/l6zrkwwgumsmegk6x4n4.png

Apple says: "Returns the smallest value for the x-coordinate of the rectangle."

Those properties have their max equivalent which return the largest coordinates rather than smallest.

// $$liquidcoderFilename-ContentView

import SwiftUI

struct ContentView1: View {

    var body: some View {
        GeometryReader { geometry in
            // highlight-start
            VStack(alignment: .leading) {
                Text("Global MidX : \(geometry.frame(in: .global).midX)")
                    .padding()

                Text("Global MidY : \(geometry.frame(in: .global).midY)")
                    .padding()

                Text("Local MidX : \(geometry.frame(in: .local).midX)")
                    .padding()

                Text("Local MidY : \(geometry.frame(in: .local).midY)")
                    .padding()
            }
            // highlight-end
        }.border(Color.red)
    }

}

https://res.cloudinary.com/liquidcoder/image/upload/v1613489759/Overview%20blog%20posts/GeometryReader/nawmaka1ubxescseyrqx.png

The CGRect object returned by the frame also has mid-coordinates (midX and midY) which are the center coordinates of the rectangle constructed by the 4 points we've just talked about.

Check out the illustration on the right which shows where those min and max coordinates are located.

Here is how they are calculated:

minX = origin.x

minY = origin.y

maxX = origin.x + size.width

maxY = origin.y + size.height

https://res.cloudinary.com/liquidcoder/image/upload/v1613574683/Overview%20blog%20posts/GeometryReader/o5tltnbecgcglmmur1yt.png

Use cases

GeometryReader can be daunting when you first start working with it, but once you get acquainted with its few rules , you will find that it's quite a fun feature to play with, and most importantly very powerful.

// $$liquidcoderFilename-ContentView

import SwiftUI

struct ContentView: View {

    var body: some View {
        GeometryReader { geometry in
            Image("image")
                .resizable()
                .scaledToFit()
                // highlight-start
                .frame(width: geometry.size.width * 0.7, height: geometry.size.height * 0.4)
                .offset(x: (geometry.size.width - (geometry.size.width * 0.7)) / 2, y: 0)
                // highlight-end
        }.border(Color.red)
    }

}

https://res.cloudinary.com/liquidcoder/image/upload/v1613502443/Overview%20blog%20posts/GeometryReader/dlylltand2vyf7vnaujk.png

We use an Image view as an example. We will center it along the x axis (horizontally), so we are only interested in the width, not the height.

We give the Image a width equal to 70% of the parent, then apply an horizontal offset equal to half of the difference between the parent's width and the child's. This will center the image no matter the parent's width.

// $$liquidcoderFilename-ContentView

import SwiftUI

struct ContentView1: View {

    var body: some View {
        ScrollView {
            ForEach(0 ..< 15) { item in
                GeometryReader { geometry in
                    // highlight-start
                    Text("""
                            Global X: \(geometry.frame(in: .global).minX) \n
                            Global Y: \(geometry.frame(in: .global).minY) \n
                            Local X: \(geometry.frame(in: .local).minX) \n
                            Local Y: \(geometry.frame(in: .local).minY) \n
                            """)
                        // highlight-end
                        .frame(width: geometry.size.width * 0.7, height: geometry.size.height )
                        .offset(x: (geometry.size.width - (geometry.size.width * 0.7)) / 2, y: 0)

                }.frame(height: 400)
            }
        }
    }

}

Take a look at this next example...We put a list of GeometryReader containers inside a scrollview. Note that we need to give each of them a fixed height for it to be displayed.

When you scroll, you will see that only the global minY changes. There are 2 reasons for that:

  1. The default scroll axis is vertical, so only y values are changing.
  2. The global coordinates are relative to the device's screen which means they reflect the origin position in the screen's rectangle.

https://res.cloudinary.com/liquidcoder/image/upload/v1613503558/Overview%20blog%20posts/GeometryReader/uieovnh1q2vgps43kfqm.gif

// $$liquidcoderFilename-ContentView

import SwiftUI

struct ContentView1: View {

    // highlight-start
    var screenHeight = UIScreen.main.bounds.height
    var screenWidth = UIScreen.main.bounds.width
    // highlight-end

    var body: some View {
        ScrollView {
            ForEach(0 ..< 15) { item in
                GeometryReader { geometry in
                    // highlight-start
                    createImage(geometry: geometry)
                    // highlight-end
                }.frame(height: 700)
            }
        }
    }

    // highlight-start
    private func createImage(geometry: GeometryProxy) -> some View {

    }
    // highlight-end
}

For the final demo, we need the entire device screen height and width. We then replace the Text view with the call to that createImage function created a little bit down.

Now let's do the remaining work in that function.

// $$liquidcoderFilename-ContentView

import SwiftUI

struct ContentView1: View {

    var screenHeight = UIScreen.main.bounds.height
    var screenWidth = UIScreen.main.bounds.width

    var body: some View {
        ScrollView {
            ForEach(0 ..< 15) { item in
                GeometryReader { geometry in
                    createImage(geometry: geometry)
                }.frame(height: 700)
            }
        }
    }

    private func createImage(geometry: GeometryProxy) -> some View {

        // highlight-start
        let height = geometry.size.height
        let minY = geometry.frame(in: .global).minY

        let scrolledInterval = max(0, min(minY, screenHeight))
        let scrolledPercent = scrolledInterval / screenHeight

        return Image("image")
            .resizable()
            .frame(width: screenWidth, height: height - (height * scrolledPercent) )
            .rotation3DEffect(
                Angle(degrees: 70 * Double(scrolledPercent)) ,
                axis: (x: 1.0, y: 0.0, z: 0.0),
                anchorZ: 0.0,
                perspective: -1.0
            ).scaleEffect(1.0 - scrolledPercent)
        // highlight-end
    }
}

The main component that makes the effect possible is the minY from the frame which is the only dynamic variable in this case.

https://res.cloudinary.com/liquidcoder/image/upload/v1613640167/Overview%20blog%20posts/GeometryReader/mgzz0e9oscdlfwdit7eb.gif

GeometryReader is one of the most powerful features in swiftUI. One can create amazing effects with it, and sky is the only limit really.