SwiftUI GeometryReader: What's your size? - Part 4

Mark Moeykens
Nov 17, 2019

This is part 4 in the GeometryReader Series. Part 3 is here.

In this article, you will learn how to get the size of the GeometryReader so you can layout views dynamically based on device size. You'll also learn how I debug GeometryReader values or calculations when testing.

Earlier in this series, you learned how to use GeometryReader to get the origin point (X, Y) of a view. The GeometryReader also exposes a height and width through a size property.

Check out these examples from SwiftUI Views book:

(By the way, this is in the free book sample so feel free to grab that book if you don't have it already.)

OK, but how would I use this in a real-world scenario?


In the next article, I will show you how to use the GeometryReader size property to create a "sticky header":


But for now, I will show you how to layout views using a certain percentage of the device size. 

Phi - The Golden Ratio

Some UX/UI designers use phi to layout their UI.

What is "phi"?

I wrote an article on this if you want to read it here. But here's an excerpt from that article:

"Phi is the name of a number that is roughly: 1.6

This number goes on but let’s keep it simple. It’s a measurement that has been observed in all kinds of nature. More accurately it is a ratio. Meaning you have the length of something in nature and something else connected to it is either 1.6 times bigger or 1.6 times smaller depending on which direction you go. Or 1.6 times longer or 1.6 times shorter."

So let's create a screen with 2 parts:
  • Top Part is 38% of the device height
  • Bottom Part is 62% of the device height

This is using the Golden Ratio because the bottom part is about 1.6 times as big as the top part.

OK, let's get started.

1. Create a new SwiftUI and let's test the capabilities of the GeometryReader first.
struct GettingSizeWithGeometryReader: View {
    var body: some View {
        GeometryReader { gr in
            VStack {
                Text("Height: \(gr.size.height)")
                Text("Width: \(gr.size.width)")
            }
        }
        .font(.largeTitle)
        .edgesIgnoringSafeArea(.vertical)
    }
}

There are a few things here to understand:
  • As you know, the GeometryReader expands to fill up all available space. You want to layout two views so the first view uses 38% of the screen. So you want the GeometryReader as the root view.
  • The GeometryReader's height will not give an accurate height of the device's available space if you're not ignoring the safe area insets. For example, the height will only be 818 on this device if I'm not using .edgesIgnoringSafeArea(.vertical). Be sure to use edgesIgnoringSafeArea modifier to get a more accurate representation of the available space.

OK, I think you have a good idea of how the size property works on the GeometryReader. Let's move on.

2. Create another SwiftUI file with two rectangles in a VStack. Give at least one of them a color so you can tell them apart.
struct LayingOutUsingPhi: View {
    var body: some View {
        VStack {
            Rectangle()
                .fill(Color.blue)
            Rectangle()
                .fill(Color.purple)
        }.edgesIgnoringSafeArea(.vertical)
    }
}

Impressive UI, right? 😃

3. Add GeometryReader at the root and remember to use the edgesIgnoringSafeArea modifier. And add a frame modifier to the first rectangle to adjust the height. 
struct LayingOutUsingPhi: View {
    var body: some View {
        GeometryReader { gr in
            VStack {
                Rectangle()
                    .fill(Color.blue)
                    // Adjust the height to be 38% of the device height
                    .frame(height: gr.size.height * 0.38)
                Rectangle()
                    .fill(Color.purple)
            }.edgesIgnoringSafeArea(.vertical)
        }
    }
}

This is great. You're on the right track now. 

Sometimes you will want to verify the numbers. My favorite way to debug GeometryReader values is to just add some Text views showing the values. Let's verify the numbers then.

4. Verify the numbers by overlaying some text views and add the geometry numbers.
struct LayingOutUsingPhi: View {
    var body: some View {
        GeometryReader { gr in
            VStack {
                Rectangle()
                    .fill(Color.blue)
                    // Adjust the height to be 38% of the device height
                    .frame(height: gr.size.height * 0.38)
                    .overlay(Text("Height: \(gr.size.height * 0.38)"))
                Rectangle()
                    .fill(Color.purple)
                    .overlay(Text("Height: \(gr.size.height * 0.62)"))
            }
            .edgesIgnoringSafeArea(.vertical)
            .font(.largeTitle)
        }
    }
}

Do these numbers look right?

No, they're not!

In the first screenshot you found out this device's height is 896. So let's do the math:

896 * 0.38 = 340.48

Can you see what I did wrong?

Yeah, I accidentally added the edgesIgnoringSafeArea modifier to the VStack and not the GeometryReader. So we were not getting the right height for the calculation.

Here is the correct code and screenshot:
struct LayingOutUsingPhi: View {
    var body: some View {
        GeometryReader { gr in
            VStack {
                Rectangle()
                    .fill(Color.blue)
                    // Adjust the height to be 38% of the device height
                    .frame(height: gr.size.height * 0.38)
                    .overlay(Text("Height: \(gr.size.height * 0.38)"))
                Rectangle()
                    .fill(Color.purple)
                    .overlay(Text("Height: \(gr.size.height * 0.62)"))
            }.font(.largeTitle)
        }.edgesIgnoringSafeArea(.vertical)
    }
}

Much better!

And as you can see, the height is relative to the device. So this will work on all devices like the iPad:

GeometryReaders inside of GeometryReaders

One last thing I wanted to mention. Say you wanted to adjust some properties on a view based on its width or height. You can use the GeometryReader in this scenario too.

For simplicity sake, say you want to change that purple rectangle to pink if the height is over 600.

You could use code like this:
.fill((gr.size.height * 0.62) > 600 ? Color.pink : Color.purple) 

Another option is you can wrap that purple rectangle in another GeometryReader and inspect the height directly. Like this: 
GeometryReader { inner_gr in
    Rectangle()
        .fill(inner_gr.size.height > 600 ? Color.pink : Color.purple)
        .overlay(Text("Height: \(inner_gr.size.height)")) 


Summary

You can see here how you can use the GeometryReader to help layout views in a relative manner. You can also change other properties of a view based on the geometry's height or width.

In the next article, you will be applying this knowledge as well as knowledge from the previous articles to build a sticky header that always stays at the top when scrolling. It's going to be exciting!

Free SwiftUI Picture Book


Screenshots for every code example? Yes! Get your FREE SwiftUI picture book here.