UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Understanding frames and coordinates inside GeometryReader

Paul Hudson    @twostraws   

SwiftUI’s GeometryReader allows us to use its size and coordinates to determine a child view’s layout, and it’s the key to creating some of the most remarkable effects in SwiftUI.

You should always keep in mind SwiftUI’s three-step layout system when working with GeometryReader: parent proposes a size for the child, the child uses that to determine its own size, and parent uses that to position the child appropriately.

In its most basic usage, what GeometryReader does is let us read the size that was proposed by the parent, then use that to manipulate our view. For example, we could use GeometryReader to make a text view have 90% of all available width regardless of its content:

struct ContentView: View {
    var body: some View {
        GeometryReader { proxy in
            Text("Hello, World!")
                .frame(width: proxy.size.width * 0.9)
                .background(.red)
        }
    }
}

That proxy parameter that comes in is a GeometryProxy, and it contains the proposed size, any safe area insets that have been applied, plus a method for reading frame values that we’ll look at in a moment.

GeometryReader has an interesting side effect that might catch you out at first: the view that gets returned has a flexible preferred size, which means it will expand to take up more space as needed. You can see this in action if you place the GeometryReader into a VStack then put some more text below it, like this:

struct ContentView: View {
    var body: some View {
        VStack {
            GeometryReader { proxy in
                Text("Hello, World!")
                    .frame(width: proxy.size.width * 0.9, height: 40)
                    .background(.red)
            }

            Text("More text")
                .background(.blue)
        }
    }
}

You’ll see “More text” gets pushed right to the bottom of the screen, because the GeometryReader takes up all remaining space. To see it in action, add background(.green) as a modifier to the GeometryReader and you’ll see just how big it is. Note: This is a preferred size, not an absolute size, which means it’s still flexible depending on its parent.

When it comes to reading the frame of a view, GeometryProxy provides a frame(in:) method rather than simple properties. This is because the concept of a “frame” includes X and Y coordinates, which don’t make any sense in isolation – do you want the view’s absolute X and Y coordinates, or their X and Y coordinates compared to their parent?

SwiftUI calls these options coordinate spaces, and those two in particular are called the global space (measuring our view’s frame relative to the whole screen), and the local space (measuring our view’s frame relative to its parent). We can also create custom coordinate spaces by attaching the coordinateSpace() modifier to a view – any children of that can then read its frame relative to that coordinate space.

To demonstrate how coordinate spaces work, we could create some example views in various stacks, attach a custom coordinate space to the outermost view, then add an onTapGesture to one of the views inside it so it can print out the frame globally, locally, and using the custom coordinate space.

Try this code:

struct OuterView: View {
    var body: some View {
        VStack {
            Text("Top")
            InnerView()
                .background(.green)
            Text("Bottom")
        }
    }
}

struct InnerView: View {
    var body: some View {
        HStack {
            Text("Left")
            GeometryReader { proxy in
                Text("Center")
                    .background(.blue)
                    .onTapGesture {
                        print("Global center: \(proxy.frame(in: .global).midX) x \(proxy.frame(in: .global).midY)")
                        print("Custom center: \(proxy.frame(in: .named("Custom")).midX) x \(proxy.frame(in: .named("Custom")).midY)")
                        print("Local center: \(proxy.frame(in: .local).midX) x \(proxy.frame(in: .local).midY)")
                    }
            }
            .background(.orange)
            Text("Right")
        }
    }
}

struct ContentView: View {
    var body: some View {
        OuterView()
            .background(.red)
            .coordinateSpace(name: "Custom")
    }
}

The output you get when that code runs depends on the device you’re using, but here’s what I got:

  • Global center: 191.33 x 440.60
  • Custom center: 191.33 x 381.60
  • Local center: 153.66 x 350.63

Those sizes are mostly different, so hopefully you can see the full range of how these frame work:

  • A global center X of 191 means that the center of the geometry reader is 191 points from the left edge of the screen.
  • A global center Y of 440 means the center of the geometry reader is 440 points from the top edge of the screen. This isn’t dead in the center of the screen because there is more safe area at the top than the bottom.
  • A custom center X of 191 means the center of the geometry reader is 191 points from the left edge of whichever view owns the “Custom” coordinate space, which in our case is OuterView because we attach it in ContentView. This number matches the global position because OuterView runs edge to edge horizontally.
  • A custom center Y of 381 means the center of the geometry reader is 381 points from the top edge of OuterView. This value is smaller than the global center Y because OuterView doesn’t extend into the safe area.
  • A local center X of 153 means the center of the geometry reader is 153 points from the left edge of its direct container.
  • A local center Y of 350 means the center of the geometry reader is 350 points from the top edge of its direct container.

Which coordinate space you want to use depends 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.
  • What to know where this view is relative to some other view? Use a custom space.
Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.0/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.