Swift and SwiftUI tutorials for Swift Developers

What is GeometryReader and how to use it in SwiftUI

In the world of modern interface development, adaptability is not a luxury; it is an absolute necessity. An iPhone SE, a 12.9-inch iPad Pro, an Apple Watch Ultra, and a resizable window on macOS have something in common: they all run your SwiftUI code, but none offer the same canvas.

This is where many novice (and not-so-novice) developers hit a wall. SwiftUI is fantastic for declarative and stackable design (VStackHStack), but what happens when you need surgical precision? What if you need a view to measure exactly one-third of the screen, or you want to create a parallax animation based on scroll position?

The answer is a powerful, often misunderstood, and sometimes feared tool: GeometryReader.

In this comprehensive tutorial, we will break down what it is, how its internal math works, and how to implement it correctly across iOS, macOS, and watchOS.


1. What is GeometryReader? Anatomy of a Container

To understand GeometryReader, we must first understand how SwiftUI renders views. Typically, in SwiftUI, parents propose a size to their children, and children choose their own size.

GeometryReader breaks this mold slightly. It is a view container that has two fundamental characteristics:

  1. It is Greedy: It attempts to fill all the space its parent offers. If you place a GeometryReader inside an empty screen, it will occupy the entire screen.
  2. It Exposes its Own Geometry: Through an object called GeometryProxy, it tells its child views: “Hey, this is the exact size I have, and this is my position on the screen.”

The GeometryProxy

When you instantiate a GeometryReader, you receive a closure with a parameter, usually named proxy or geometry.

GeometryReader { proxy in
    // Your code here
}

This proxy is the master key. It contains:

  • size: A CGSize with the width and height of the container.
  • safeAreaInsets: The safe margins (the notch, the home bar, etc.).
  • frame(in: CoordinateSpace): The most powerful method, which allows you to know where the view is relative to different coordinate systems.

2. Basic Implementation: Relative Dimensions

The most common use of GeometryReader is sizing elements as a percentage of available space. Imagine you want two rectangles: one occupying 60% of the width and another 40%.

struct ProgressBar: View {
    var body: some View {
        GeometryReader { geometry in
            HStack(spacing: 0) {
                Rectangle()
                    .fill(Color.blue)
                    .frame(width: geometry.size.width * 0.6)
                
                Rectangle()
                    .fill(Color.gray)
                    .frame(width: geometry.size.width * 0.4)
            }
        }
        .frame(height: 50) // We limit the height because GeometryReader tries to expand
    }
}

Critical Note: Observe the .frame(height: 50) at the end. If you don’t include it, the GeometryReader will attempt to occupy the entire vertical height of the screen, pushing other elements out or creating empty spaces.


3. Going Deeper: Coordinate Spaces

This is where the magic (and the confusion) happens. GeometryReader doesn’t just tell you how big something is, but whereit is. But where is it relative to what?

SwiftUI handles three main coordinate spaces:

  1. .local: Coordinates relative to the view itself (the 0,0 origin is the top-left corner of the view).
  2. .global: Coordinates relative to the entire device screen.
  3. .named(“Name”): Coordinates relative to a specific ancestor view that you have tagged.

Practical Example: Parallax Effect

Let’s create a list in iOS where images change size or position smoothly while the user scrolls. This effect is achieved by measuring the Y position of each cell in the global space.

struct ParallaxView: View {
    var body: some View {
        ScrollView {
            VStack(spacing: 20) {
                ForEach(0..<10) { _ in
                    GeometryReader { proxy in
                        let minY = proxy.frame(in: .global).minY
                        
                        Image("landscape")
                            .resizable()
                            .aspectRatio(contentMode: .fill)
                            // Math magic for parallax
                            .offset(y: -minY * 0.5) 
                            .frame(width: proxy.size.width, height: proxy.size.height)
                            .clipped()
                    }
                    .frame(height: 200)
                }
            }
        }
    }
}

In this code, proxy.frame(in: .global).minY tells us how far the top of the image is from the top edge of the screen. We use that value to offset the internal image in the opposite direction, creating the illusion of depth.


4. Multiplatform Strategies: iOS, macOS, and watchOS

Although the code is the same, the application of GeometryReader varies by device.

macOS: Resizable Windows

On macOS, the user can change the window size at any time. GeometryReader is vital here for rearranging complex layouts.

Imagine a sidebar that should collapse if the window width is less than 500px.

// Conceptual example for macOS
GeometryReader { proxy in
    HStack {
        if proxy.size.width > 500 {
            SidebarView()
                .frame(width: 200)
        }
        MainContentView()
    }
}

Tip: On macOS, resizing triggers many view updates. Keep the logic inside the GeometryReader lightweight to avoid FPS drops.

watchOS: Space is Gold

On the Apple Watch, screens are tiny. Using GeometryReader to calculate dynamic text sizes or circular charts (like Activity Rings) is very common.

// Fitting an activity ring to the watch width
GeometryReader { geometry in
    ZStack {
        Circle()
            .stroke(lineWidth: 10)
            .foregroundColor(.gray.opacity(0.3))
        
        Circle()
            .trim(from: 0.0, to: 0.7)
            .stroke(style: StrokeStyle(lineWidth: 10, lineCap: .round))
            .foregroundColor(.green)
            .rotationEffect(.degrees(-90))
    }
    // We use the smaller of the two values to ensure a perfect circle
    .frame(width: min(geometry.size.width, geometry.size.height)) 
}

5. The “Dark Side”: Common Mistakes and How to Avoid Them

The biggest mistake when using GeometryReader is forgetting that it breaks the natural layout of SwiftUI.

The Center Alignment Problem

Normally, a VStack centers its contents. But if you put Text inside a GeometryReader, the text will jump to the top-left corner (0,0).

Solution: You must manually position elements within the reader or use a container that centers them.

GeometryReader { proxy in
    Text("Hello World")
        .position(x: proxy.size.width / 2, y: proxy.size.height / 2)
}

The Layout Loop

If you use GeometryReader to change a @State variable which in turn changes the size of the view containing the GeometryReader, you will create an infinite loop, and the app will freeze or crash.

Golden Rule: Avoid updating @State directly from the body of a GeometryReader without precautions. If you need to pass size data up the hierarchy, use PreferencesKey.


6. Advanced Technique: Reading Sizes Without Altering Layout

Sometimes you just want to know how big a text is to draw an underline, but you don’t want a GeometryReader to expand your view and ruin your design.

The solution is to use GeometryReader in the background or overlay. This is a pro technique.

struct MeasuredText: View {
    @State private var textHeight: CGFloat = 0
    
    var body: some View {
        Text("This is dynamic text")
            .padding()
            .background(
                GeometryReader { proxy in
                    Color.clear // Invisible
                        .onAppear {
                            textHeight = proxy.size.height
                        }
                        .onChange(of: proxy.size) { newSize in
                            textHeight = newSize.height
                        }
                }
            )
        
        Text("The text height above is: \(textHeight)")
    }
}

By placing it in the .background, the GeometryReader adopts the size of the Text (because the background always matches the view’s size), allowing us to read its dimensions without affecting the visual flow.


7. Modern Alternatives: Is It Still Necessary?

With iOS 16 and later versions, Apple introduced the Layout protocol and the ViewThatFits view.

  • ViewThatFits: Allows choosing between two views depending on available space, eliminating the need for GeometryReader for simple adaptive layouts.
  • Layout Protocol: Allows creating custom containers with complex mathematical logic much more efficiently than a GeometryReader.

However, for scroll-based animationsdrawing complex Paths, and exact relative positioningGeometryReader remains the undisputed king.


Conclusion

GeometryReader is a Swiss Army Knife. In inexperienced hands, it can cut the flow of your design and cause headaches with broken layouts. But in expert hands, it is the tool that enables those fluid, living, and perfectly aligned interfaces that characterize the best apps on the App Store.

Summary of Best Practices:

  1. Use it only when VStack/HStack/Spacer are not enough.
  2. Remember it is “greedy” and will fill all space; limit it with .frame() if necessary.
  3. Be careful with .global vs .local coordinates.
  4. To read sizes without affecting layout, use it in a .background().

Now you have the knowledge. Open Xcode and start breaking the boundaries of static layout.

If you have any questions about this article, please contact me and I will be happy to help you 🙂. You can contact me on my X profile or on my Instagram profile.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

How to animate SF Symbols in SwiftUI

Next Article

Best SwiftUI frameworks

Related Posts