← Back to archive

Development · SwiftUI

Reading layout with GeometryReader

Use available size and coordinate spaces to build layouts that respond to their actual container.

Read the Korean original on Velog ↗English translation

Development archive cover

A layout based only on the device's screen size may not adapt to split-screen use or a changing parent view. GeometryReader provides information about the space available to the view itself.

UIScreen

Screen dimensions can be used to calculate widths and positions, but they do not necessarily describe the current container. This makes screen-based calculations fragile when the window or layout changes.

HStack(spacing: 0) {
    Rectangle()
        .fill(.red)
        // Two-thirds of the screen width
        .frame(width: UIScreen.main.bounds.width * 0.666)
    Rectangle()
        .fill(.blue)
}
.ignoresSafeArea()

GeometryReader

The GeometryProxy supplied to the closure exposes size and coordinate information. Use it to calculate a child's layout within the available space. Because geometry-driven changes can trigger additional layout work, use it where that information is actually needed.

GeometryReader { geometry in
    HStack(spacing: 0) {
        Rectangle()
            .fill(.red)
            // Two-thirds of the parent's width
            .frame(width: geometry.size.width * 0.666)
        Rectangle()
            .fill(.blue)
    }
    .ignoresSafeArea()
}

Reading a frame

frame(in:) returns the view's frame in a specified coordinate space.

enum CoordinateSpace {
  case global // Relative to the global coordinate space
  case local // Relative to the GeometryReader
  case named(AnyHashable) // Relative to the named coordinate space
}    

Geometry information can also drive visual effects as a view changes position.

struct GeometryReaderStudy: View {
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack {
                ForEach(0..<20) { index in
                    GeometryReader { geometry in
                        RoundedRectangle(cornerRadius: 20)
                            .rotation3DEffect(
                            	// Rotate the card based on its position
                                Angle(degrees: getPercentage(geo: geometry)) * 20,
                                axis: (x: 0.0, y: 1.0, z: 0.0))
                    }
                    .frame(width: 300, height: 250)
                    .padding()
                }
            }
        }
    }
    func getPercentage(geo: GeometryProxy) -> Double {
    	// The screen midpoint
        let maxDistance = UIScreen.main.bounds.width / 2
        // The card's current midpoint in global coordinates
        let currentX = geo.frame(in: .global).midX
        // Calculate the relative position
        return Double(1 - (currentX / maxDistance))
    }
}

← Back to archive