GeometryReader
Definition
GeometryReader
is a container view in SwiftUI that provides access to the size and coordinate space of its parent. It's essential for creating responsive layouts and custom views that adapt to different screen sizes.
Basic Syntax
GeometryReader { geometry in
// Use geometry.size.width and geometry.size.height
Text("Size: \(geometry.size.width) x \(geometry.size.height)")
}
Understanding GeometryProxy
GeometryReader { geometry in
VStack {
Text("Width: \(geometry.size.width, specifier: "%.0f")")
Text("Height: \(geometry.size.height, specifier: "%.0f")")
Text("Safe Area Top: \(geometry.safeAreaInsets.top, specifier: "%.0f")")
Text("Safe Area Bottom: \(geometry.safeAreaInsets.bottom, specifier: "%.0f")")
}
}
Responsive Layout
struct ResponsiveGrid: View {
var body: some View {
GeometryReader { geometry in
let columns = Int(geometry.size.width / 150) // Adaptive columns
let itemWidth = geometry.size.width / CGFloat(columns) - 10
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: max(1, columns))) {
ForEach(0..<20, id: \.self) { index in
RoundedRectangle(cornerRadius: 8)
.fill(Color.blue.opacity(0.7))
.frame(width: itemWidth, height: itemWidth)
.overlay(
Text("\(index)")
.foregroundColor(.white)
.font(.headline)
)
}
}
.padding()
}
}
}
Proportional Sizing
struct ProportionalLayout: View {
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
// Header takes 20% of height
Rectangle()
.fill(Color.red)
.frame(height: geometry.size.height * 0.2)
.overlay(Text("Header").foregroundColor(.white))
// Content takes 60% of height
Rectangle()
.fill(Color.blue)
.frame(height: geometry.size.height * 0.6)
.overlay(Text("Content").foregroundColor(.white))
// Footer takes 20% of height
Rectangle()
.fill(Color.green)
.frame(height: geometry.size.height * 0.2)
.overlay(Text("Footer").foregroundColor(.white))
}
}
}
}
Center Positioning
struct CenteredView: View {
var body: some View {
GeometryReader { geometry in
Circle()
.fill(Color.orange)
.frame(width: 100, height: 100)
.position(
x: geometry.size.width / 2,
y: geometry.size.height / 2
)
}
}
}
Custom Grid Layout
struct CustomGrid: View {
let items = Array(0..<12)
var body: some View {
GeometryReader { geometry in
let columns = 3
let spacing: CGFloat = 10
let itemWidth = (geometry.size.width - CGFloat(columns + 1) * spacing) / CGFloat(columns)
LazyVStack(spacing: spacing) {
ForEach(0..<(items.count + columns - 1) / columns, id: \.self) { row in
HStack(spacing: spacing) {
ForEach(0..<columns, id: \.self) { column in
let index = row * columns + column
if index < items.count {
RoundedRectangle(cornerRadius: 8)
.fill(Color.purple.opacity(0.7))
.frame(width: itemWidth, height: itemWidth)
.overlay(
Text("\(items[index])")
.foregroundColor(.white)
)
} else {
Color.clear
.frame(width: itemWidth, height: itemWidth)
}
}
}
}
}
.padding(spacing)
}
}
}
Coordinate Space Conversion
struct CoordinateExample: View {
@State private var location: CGPoint = .zero
var body: some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.gray.opacity(0.3))
.overlay(
Circle()
.fill(Color.red)
.frame(width: 20, height: 20)
.position(location)
)
.gesture(
DragGesture(coordinateSpace: .local)
.onChanged { value in
location = value.location
}
)
.overlay(
VStack {
Text("Tap anywhere")
Text("X: \(location.x, specifier: "%.0f")")
Text("Y: \(location.y, specifier: "%.0f")")
}
.padding()
.background(Color.white.opacity(0.8))
.cornerRadius(8),
alignment: .topLeading
)
}
}
}
Aspect Ratio Calculations
struct AspectRatioView: View {
var body: some View {
GeometryReader { geometry in
let aspectRatio: CGFloat = 16/9
let maxWidth = geometry.size.width
let maxHeight = geometry.size.height
// Calculate size maintaining aspect ratio
let calculatedWidth: CGFloat
let calculatedHeight: CGFloat
if maxWidth / aspectRatio <= maxHeight {
calculatedWidth = maxWidth
calculatedHeight = maxWidth / aspectRatio
} else {
calculatedWidth = maxHeight * aspectRatio
calculatedHeight = maxHeight
}
Rectangle()
.fill(Color.blue)
.frame(width: calculatedWidth, height: calculatedHeight)
.position(
x: geometry.size.width / 2,
y: geometry.size.height / 2
)
.overlay(
Text("16:9 Aspect Ratio")
.foregroundColor(.white)
.font(.headline)
)
}
}
}
Device Orientation Handling
struct OrientationAwareView: View {
var body: some View {
GeometryReader { geometry in
let isLandscape = geometry.size.width > geometry.size.height
if isLandscape {
HStack {
sidebar
mainContent
}
} else {
VStack {
mainContent
sidebar
}
}
}
}
private var sidebar: some View {
Rectangle()
.fill(Color.gray.opacity(0.3))
.overlay(Text("Sidebar"))
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
private var mainContent: some View {
Rectangle()
.fill(Color.blue.opacity(0.3))
.overlay(Text("Main Content"))
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
Safe Area Handling
struct SafeAreaExample: View {
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
// Status bar area
Rectangle()
.fill(Color.red)
.frame(height: geometry.safeAreaInsets.top)
.overlay(
Text("Status Bar Area")
.foregroundColor(.white)
.font(.caption)
)
// Main content
Rectangle()
.fill(Color.blue)
.frame(
height: geometry.size.height -
geometry.safeAreaInsets.top -
geometry.safeAreaInsets.bottom
)
.overlay(
Text("Safe Content Area")
.foregroundColor(.white)
)
// Home indicator area
Rectangle()
.fill(Color.green)
.frame(height: geometry.safeAreaInsets.bottom)
.overlay(
Text("Home Indicator")
.foregroundColor(.white)
.font(.caption)
)
}
}
.ignoresSafeArea()
}
}
Custom Shapes with GeometryReader
struct CustomWave: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
let width = rect.width
let height = rect.height
path.move(to: CGPoint(x: 0, y: height * 0.7))
path.addCurve(
to: CGPoint(x: width, y: height * 0.3),
control1: CGPoint(x: width * 0.3, y: height * 0.1),
control2: CGPoint(x: width * 0.7, y: height * 0.9)
)
path.addLine(to: CGPoint(x: width, y: height))
path.addLine(to: CGPoint(x: 0, y: height))
path.closeSubpath()
return path
}
}
struct WaveView: View {
var body: some View {
GeometryReader { geometry in
CustomWave()
.fill(
LinearGradient(
gradient: Gradient(colors: [.blue, .purple]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
}
.frame(height: 200)
}
}
Performance with GeometryReader
// Efficient: Extract subviews to avoid recomputation
struct EfficientGeometryView: View {
var body: some View {
GeometryReader { geometry in
ContentView(width: geometry.size.width)
}
}
}
struct ContentView: View {
let width: CGFloat
var body: some View {
// Heavy computation happens here only when width changes
let columns = Int(width / 120)
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: columns)) {
ForEach(0..<100, id: \.self) { index in
Text("Item \(index)")
.frame(height: 50)
.background(Color.blue.opacity(0.3))
}
}
}
}
Common Pitfalls
// ❌ Don't do this - GeometryReader takes all available space
VStack {
Text("Header")
GeometryReader { geometry in
Text("This will expand to fill remaining space")
}
Text("Footer") // This might not be visible
}
// ✅ Better approach - Use frame to limit size
VStack {
Text("Header")
GeometryReader { geometry in
Text("Constrained content")
}
.frame(height: 200) // Explicit height
Text("Footer")
}
Best Practices
- Use sparingly: GeometryReader can impact performance if overused
- Extract subviews: Move complex calculations to separate views
- Avoid nesting: Multiple nested GeometryReaders can cause layout issues
- Consider alternatives: Often VStack, HStack, and frame modifiers suffice
- Test on different devices: Verify layouts work across screen sizes
- Handle safe areas: Account for notches and home indicators
- Use PreferenceKey: For passing size information up the view hierarchy
Common Use Cases
- Responsive grid layouts
- Custom drawing and shapes
- Proportional sizing
- Centering content
- Adaptive UI for different orientations
- Custom containers and layouts
- Size-aware components