An "80%-width card" or a "2-up grid cell" needs to know its container's size before it can compute its own. The classic tool for that, GeometryReader, is greedy — it expands to fill whatever space it's given and forces you to thread sizes back down manually.
#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 extra .frame(height: 180) on the outside is there only to stop GeometryReader from swallowing the parent layout.
#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)
}
}
count: 5, span: 4 means each card claims four of five equal slices of the container's width — an 80% card with a peek of the next one, computed for you.
#Why it matters
- No
GeometryReadernesting, no manual size propagation, no layout thrashing. - The
count/spanform gives you grid math (2-up, 3-up, 80%-width) without doing the division yourself. - Recalculates automatically on rotation, split view resize, and Dynamic Type-driven layout changes.
- Works equally well inside
LazyVGridcells and plainVStackcontent, not just horizontal carousels.
#Gotcha
"Container" means the nearest enclosing scroll view (or List), not necessarily the view's immediate parent. If you use .containerRelativeFrame(.horizontal, count: 2) inside a plain HStack that isn't itself inside a ScrollView, the reference size comes from further up the hierarchy — the enclosing scroll view or window — not the HStack's own bounds. Sizes that look right in one layout context can come out wrong after you move the same view into a different parent.