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.