← All shorts
08iOS 17+2 min read

containerRelativeFrame: Sizing Children as a Fraction of Their Container

Size a view as a percentage of its scroll view or container without a GeometryReader in sight.

An "80%-width card" needs to know its container's width before it can work out its own. Same for a "2-up grid cell".

GeometryReader is the classic answer. It is also greedy: hand it a slot and it expands to fill whatever space it was given, which means you end up pinning it down with an explicit frame and then threading sizes back down the tree by hand.

#The old way

struct OldRelativeCard: View {
    var body: some View {
        GeometryReader { geometry in
            RoundedRectangle(cornerRadius: 16)
                .fill(.blue.gradient)
                .frame(width: geometry.size.width * 0.8, height: 180)
        }
        .frame(height: 180)
    }
}

That outer .frame(height: 180) does no design work at all; it is there to stop GeometryReader from swallowing the parent layout. Pure scaffolding.

#The new way

import SwiftUI

struct FeaturedProduct: Identifiable {
    let id = UUID()
    let name: String
}

struct FeaturedCarousel: View {
    let products: [FeaturedProduct] = [
        FeaturedProduct(name: "Wireless Headphones"),
        FeaturedProduct(name: "Mechanical Keyboard"),
        FeaturedProduct(name: "Standing Desk"),
        FeaturedProduct(name: "4K Webcam")
    ]

    var body: some View {
        ScrollView(.horizontal) {
            LazyHStack(spacing: 16) {
                ForEach(products) { product in
                    RoundedRectangle(cornerRadius: 20)
                        .fill(.indigo.gradient)
                        .overlay(alignment: .bottomLeading) {
                            Text(product.name)
                                .font(.headline)
                                .foregroundStyle(.white)
                                .padding()
                        }
                        .containerRelativeFrame(.horizontal, count: 5, span: 4, spacing: 16)
                }
            }
            .scrollTargetLayout()
        }
        .scrollTargetBehavior(.viewAligned)
    }
}

Read count: 5, span: 4 as an instruction. Cut the container's width into five equal slices, hand each card four of them, and you get an 80% card with a strip of the next one showing at the trailing edge. SwiftUI does the division.

#Why it matters

No GeometryReader nesting. That kills manual size propagation and the layout thrashing that comes with it, and the count/span form hands you grid math straight from the call site: 2-up, 3-up, 80%-width.

Sizes recalculate on rotation, on split view resize, and on Dynamic Type-driven layout changes, without you wiring anything up. The same call works inside LazyVGrid cells and plain VStack content as it does in a horizontal carousel.

#Gotcha

"Container" means the nearest enclosing scroll view, or List. Not the immediate parent. Drop .containerRelativeFrame(.horizontal, count: 2) into a plain HStack that isn't itself inside a ScrollView and the reference size skips the HStack's own bounds entirely, coming instead from further up the hierarchy, off the enclosing scroll view or the window. So a view that measures correctly in one layout can come out wrong the moment you move it under a different parent.

swiftuiios17scrolling

Related shorts