What type does a ViewBuilder body actually produce for modifiers, if, several views and AnyView, and why does it matter?
Predict first
What types do these builders produce?
import SwiftUI
struct Probe: View { var body: some View { Text("hi").padding().background(Color.red) } }
struct Cond: View { var flag: Bool; var body: some View { if flag { Text("a") } else { Image(systemName: "star") } } }
struct Tup: View { var body: some View { Text("a"); Text("b") } }
struct Many: View {
var body: some View {
Text("1"); Text("2"); Text("3"); Text("4"); Text("5"); Text("6")
Text("7"); Text("8"); Text("9"); Text("10"); Text("11"); Text("12")
}
}
struct Erased: View { var flag: Bool; var body: some View { flag ? AnyView(Text("a")) : AnyView(Image(systemName: "star")) } }
struct Opt: View { var show: Bool; var body: some View { if show { Text("x") } } }
struct Loop: View { var body: some View { ForEach([1, 2], id: \.self) { Text("\($0)") } } }
func name<T>(_ t: T) -> String {
String(reflecting: T.self).replacingOccurrences(of: "SwiftUI.", with: "").replacingOccurrences(of: "SwiftUICore.", with: "")
}
print("modifiers:", name(Probe().body))
print("if/else:", name(Cond(flag: true).body))
print("two views:", name(Tup().body))
print("twelve views:", name(Many().body))
print("AnyView:", name(Erased(flag: true).body))
print("lone if:", name(Opt(show: true).body))
print("ForEach:", name(Loop().body))
- nested ModifiedContent, _ConditionalContent, a tuple of 12 compiles, AnyView, Optional, ForEach
- every body is just a View
- twelve children do not compile
Mental model. The body is a Russian doll built at compile time: every modifier and branch is another doll around the first, and AnyView is opaque wrapping that hides which doll is inside.
Printing the type of body shows what the compiler built (SDK 27, macOS check). Modifiers nest: ModifiedContent<ModifiedContent<Text, _PaddingLayout>, _BackgroundStyleModifier<Color>>. An if/else becomes _ConditionalContent<Text, Image>, one type with two cases, so SwiftUI can tell which branch is live and treats a switch as remove and insert (identity change). Several statements become a tuple container; on this SDK it printed as a pack of the child types, and twelve children compiled, so the old limit of ten views per builder is gone with variadic generics (older SDKs print TupleView<(Text, Text)> and stop at ten). A lone if becomes Optional<Text>, ForEach keeps its data and id types, and AnyView is just AnyView.
Why care: the static type is how SwiftUI diffs cheaply and keeps identity (the branch is part of the structure, Q260). AnyView hides the type, so SwiftUI must compare more dynamically, and the two branches share one identity position: a ternary of two AnyViews can keep state across the switch where if/else would not, but loses animation and diff precision. Prefer @ViewBuilder functions, Group and conditional modifiers (.opacity) to AnyView, and use it at real type-erasure boundaries (heterogeneous arrays, plug-ins). The names of these private types are not API: print them to learn, never to depend on them.
The builder creates nested generic types.
Common traps
- Using
AnyViewto dodge a type error. It costs diff precision and hides structure. - Relying on the private type names. They differ between SDKs.
They might follow up with
- Is there still a 10-view limit? Not on SDKs with variadic generics, as checked here.
- Why can an
AnyViewternary keep state? Both branches occupy one structural position.
Related questions
- 8.1.1 What is a View, what does some View mean, and why do modifiers wrap rather than mutate?
- 8.1.2 What is structural versus explicit identity in SwiftUI, and what happens to state when identity changes?
- 1.4.2 some View vs any View — what is the difference, and why does SwiftUI use some?
How do you save and restore a NavigationPath, and what can go wrong? →