Issue #1015
Building a container view that wraps child views and places dividers between them sounds simple. But until iOS 18, SwiftUI didn’t give us a clean way to iterate over child views. Let’s look at how this problem was solved before, and the elegant solution Apple now provides.
Imagine you want a component like this:
SeparatedGroup {
Text("First item")
Text("Second item")
Text("Third item")
}
That renders three text views with a Divider() between each pair. The tricky part? SwiftUI’s @ViewBuilder gives you a single opaque Content type. You can’t loop through individual children directly.
The Legacy Approach: _VariadicView
Before iOS 18, developers reached for Apple’s private _VariadicView API. It worked, but came with a warning: private APIs can break without notice.
Here’s how it looked:
struct SeparatedGroup<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
_VariadicView.Tree(SeparatorLayout()) {
content
}
}
}
struct SeparatorLayout: _VariadicView_MultiViewRoot {
func body(children: _VariadicView.Children) -> some View {
VStack(spacing: 0) {
ForEach(Array(children.enumerated()), id: \.offset) { index, child in
if index > 0 {
Divider()
}
child
}
}
}
}
The _VariadicView.Children collection gave us access to each child view. We could enumerate them, check indices, and insert dividers where needed.
This worked fine in production apps. But relying on underscored APIs always felt risky. Apple could rename or remove them at any time.
The Modern Way: Group(subviews:)
iOS 18 introduced Group(subviews:) as the official replacement. It’s clean, documented, and safe to use.
Let’s rebuild our separator component:
struct SeparatedGroup<Content: View>: View {
@ViewBuilder let content: Content
var body: some View {
VStack(spacing: 0) {
Group(subviews: content) { subviews in
ForEach(Array(subviews.enumerated()), id: \.offset) { index, subview in
if index > 0 {
Divider()
}
subview
}
}
}
}
}
The Group(subviews:) initializer takes your content and provides a SubviewsCollection in its closure. This collection behaves like any Swift collection—you can iterate, enumerate, check count, or access elements by index.
Each element in the collection is a Subview, which acts as a proxy to the actual view. You can place it directly in your layout.
Using ForEach(subviews:)
There’s also ForEach(subviews:) for cases where you want direct iteration:
struct CardStack<Content: View>: View {
@ViewBuilder let content: Content
var body: some View {
VStack(spacing: 12) {
ForEach(subviews: content) { subview in
subview
.padding()
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
}
}
This wraps each child in a card-style container. No manual enumeration needed when you don’t care about indices.
Working with Sections: ForEach(sections:)
SwiftUI also provides ForEach(sections:) for containers that need to respect Section boundaries. Each iteration gives you a SectionConfiguration with header, content, and footer properties.
struct CustomList<Content: View>: View {
@ViewBuilder let content: Content
var body: some View {
VStack(alignment: .leading, spacing: 16) {
ForEach(sections: content) { section in
VStack(alignment: .leading, spacing: 8) {
section.header
.font(.headline)
ForEach(subviews: section.content) { item in
item
.padding(.leading)
}
section.footer
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
}
You can then use it with standard Section syntax:
CustomList {
Section("Fruits") {
Text("Apple")
Text("Banana")
}
Section("Vegetables") {
Text("Carrot")
Text("Broccoli")
}
}
The container receives each section separately, preserving your content’s logical structure.
Here’s a practical example—a form group that adds dividers and consistent styling:
struct FormGroup<Content: View>: View {
@ViewBuilder let content: Content
var body: some View {
VStack(spacing: 0) {
Group(subviews: content) { subviews in
ForEach(Array(subviews.enumerated()), id: \.offset) { index, subview in
if index > 0 {
Divider()
.padding(.leading, 44)
}
subview
.frame(minHeight: 44)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
.padding()
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
Usage feels natural:
FormGroup {
Label("Profile", systemImage: "person")
Label("Settings", systemImage: "gear")
Label("Help", systemImage: "questionmark.circle")
}
The new container APIs in iOS 18 solve a long-standing SwiftUI limitation. You no longer need private APIs to build custom containers that transform their children.
Group(subviews:)— Access children as a collection for custom layoutsForEach(subviews:)— Iterate directly over childrenForEach(sections:)— Iterate over sections with header/footer access
These tools open up possibilities for building reusable, composable UI components. Whether you’re creating separated lists, card stacks, or custom form layouts, you now have first-class support from SwiftUI itself.
Start the conversation