A container view holds views the caller supplies through a @ViewBuilder closure. The
iOS 18 era decomposition APIs, ForEach(subviews:) and Group(subviews:), let the container
pull those children apart and lay them out its own way.
Plain containers with @ViewBuilder
The classic form wraps the content without touching its children. This is enough when the container only adds chrome around the whole group.
struct Card<Content: View>: View {
@ViewBuilder var content: Content
var body: some View {
VStack { content }
.padding()
.background(Material.regular, in: .rect(cornerRadius: 8))
.shadow(radius: 4)
}
}
Iterate children with ForEach(subviews:)
ForEach(subviews:) extracts the children of a content view and hands each one to you as a
Subview. A Subview conforms to View, so modifiers still attach, and it carries a stable
id plus its container values.
struct Carousel<Content: View>: View {
@ViewBuilder var content: Content
var body: some View {
ScrollView(.horizontal) {
LazyHStack {
ForEach(subviews: content) { subview in
subview.containerRelativeFrame(.horizontal)
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.viewAligned)
.contentMargins(16)
}
}
Index children with Group(subviews:)
Group(subviews:) gives the whole SubviewsCollection at once. It conforms to
RandomAccessCollection, so you can treat the first child differently and slice the rest.
struct Magazine<Content: View>: View {
@ViewBuilder var content: Content
var body: some View {
ScrollView {
Group(subviews: content) { subviews in
if !subviews.isEmpty {
subviews[0]
.containerRelativeFrame(.vertical) { length, _ in length / 3 }
}
if subviews.count > 1 {
ScrollView(.horizontal) {
LazyHStack {
ForEach(subviews[1...], id: \.id) { $0.containerRelativeFrame([.horizontal, .vertical]) }
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.viewAligned)
}
}
}
}
}
Guard the indexed access: check isEmpty and count before subscripting, because the caller
decides how many children exist.