Mastering SwiftUI Container
With the release of iOS 18, SwiftUI introduces a powerful new way to create custom container views.
Before diving into the implementation, it’s essential to understand the distinction between declared subviews and resolved subviews. Let’s take a look at the following example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct MyView: View {
let items = [
"Amount of real estate taxes paid",
"Amount of personal property taxes paid"
]
var body: some View {
List {
Text("W2")
Text("1099-B")
Text("1099-R")
Text("1099-DIV")
Text("1099-INT")
Text("1099-MISC")
ForEach(items, id: \.self) { item in
Text(item)
}
}
}
}
In this example, the List
contains 7 declared subviews: six Text
views defined explicitly and one ForEach
view. However, at runtime, these translate into 8 resolved subviews: the six declared Text
views plus two additional Text
views generated by the ForEach
.
In SwiftUI’s declarative framework, declared subviews serve as a blueprint for creating resolved subviews while the app is running. For instance:
- A
ForEach
view is a declared subview that doesn’t have a specific visual representation by itself. Its sole purpose is to generate a collection of resolved subviews. - Similarly, a
Group
view acts as a declared container for resolved subviews. For example, aGroup
containing threeText
views resolves into exactly three subviews. - Some declared subviews, such as
EmptyView
, may resolve to no subviews at all. Others, like conditional views (if
statements), may dynamically resolve into different numbers of subviews based on runtime conditions.
Building a Custom Container
The new ForEach(subviews:)
API allows us to easily iterate over resolved subviews. This makes it simpler to build custom containers, as SwiftUI handles the resolution of subviews for us, regardless of how they’re declared. For example, here’s how we can implement a custom Checklist
container:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
struct MyView: View {
let items = [
"Amount of real estate taxes paid",
"Amount of personal property taxes paid"
]
var body: some View {
Checklist {
Text("W2")
Text("1099-B")
Text("1099-R")
Text("1099-DIV")
Text("1099-INT")
Text("1099-MISC")
ForEach(items, id: \.self) { item in
Text(item)
}
}
}
}
struct Checklist<Content: View>: View {
@ViewBuilder var content: Content
var body: some View {
ScrollView {
VStack {
ForEach(subviews: content) { subview in
ChecklistRow {
subview
}
}
}
}
.contentMargins(16, for: .scrollContent)
}
}
struct ChecklistRow<Content: View>: View {
@ViewBuilder var content: Content
@State private var checked = false
var body: some View {
Button {
checked.toggle()
} label: {
Label {
content
} icon: {
Image(systemName: checked ? "checkmark.circle.fill" : "circle")
}
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(.rect)
}
.buttonStyle(.plain)
}
}
Reading Subview Count
If you need to access the count of resolved subviews (e.g., for styling based on indices), you can use the Group(subviews:)
API. This works similarly to ForEach(subviews:)
, but instead of iterating over subviews, it provides a collection of all resolved subviews.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Checklist<Content: View>: View {
@ViewBuilder var content: Content
var body: some View {
ScrollView {
VStack {
Group(subviews: content) { subviews in
ForEach(Array(subviews.enumerated()), id: \.element.id) { (idx, subview) in
ChecklistRow {
subview
}
.background(idx.isMultiple(of: 2) ? Color.secondary.opacity(0.25) : Color.clear)
}
}
}
}
.contentMargins(16, for: .scrollContent)
}
}
Adding Section Support
Built-in containers like List
support sections via the Section
view, which allows for headers and footers. By default, custom containers don’t support sections, but you can add this functionality using the ForEach(sections:)
API.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
struct MyView: View {
let items = [
"Amount of real estate taxes paid",
"Amount of personal property taxes paid"
]
var body: some View {
Checklist {
Section {
Text("W2")
} header: {
Text("Primary income")
}
Section {
Text("1099-B")
Text("1099-R")
Text("1099-DIV")
Text("1099-INT")
Text("1099-MISC")
} header: {
Text("1099s")
}
Section {
ForEach(items, id: \.self) { item in
Text(item)
}
} header: {
Text("Misc")
}
}
}
}
struct Checklist<Content: View>: View {
@ViewBuilder var content: Content
var body: some View {
ScrollView {
VStack {
ForEach(sections: content) { sections in
VStack {
sections.header
.font(.headline)
.textCase(.uppercase)
list(sections.content)
sections.footer
.font(.footnote)
}
}
}
}
.contentMargins(16, for: .scrollContent)
}
@ViewBuilder func list<Subviews: View>(_ subviews: Subviews) -> some View {
ForEach(subviews: subviews) { subview in
ChecklistRow {
subview
}
}
}
}
Container-Specific Modifiers
SwiftUI introduces container values, a new type of keyed storage similar to environment values. However, unlike environment values (which propagate down the entire view hierarchy) or preferences (which propagate upward), container values are scoped to their immediate container, making them perfect for container-specific customizations.
Here’s an example of defining and using container values:
1
2
3
4
5
6
7
8
9
10
11
extension ContainerValues {
@Entry var notApplicable: Bool = false
}
extension View {
func notApplicable() -> some View {
self.containerValue(\.notApplicable, true)
}
}
We can then apply this modifier to certain views and access it in the container:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
struct MyView: View {
let items = [
"Amount of real estate taxes paid",
"Amount of personal property taxes paid"
]
var body: some View {
Checklist {
Section {
Text("W2")
} header: {
Text("Primary income")
}
Section {
Text("1099-B")
Text("1099-R")
Text("1099-DIV")
Text("1099-INT")
Text("1099-MISC")
} header: {
Text("1099s")
}
Section {
ForEach(items, id: \.self) { item in
Text(item)
}
.notApplicable()
} header: {
Text("Misc")
}
}
}
}
struct Checklist<Content: View>: View {
@ViewBuilder var content: Content
var body: some View {
ScrollView {
VStack {
ForEach(sections: content) { sections in
VStack {
sections.header
.font(.headline)
.textCase(.uppercase)
list(sections.content)
sections.footer
.font(.footnote)
}
}
}
}
.contentMargins(16, for: .scrollContent)
}
@ViewBuilder func list<Subviews: View>(_ subviews: Subviews) -> some View {
ForEach(subviews: subviews) { subview in
ChecklistRow {
subview
.strikethrough(subview.containerValues.notApplicable)
}
}
}
}
With these tools, SwiftUI’s new container APIs make building flexible, reusable custom views easier and more powerful than ever.