What SwiftUI Is Made Of
Genre: framework-explainer. The post explains the substrate that SwiftUI sits on top of: result builders, opaque return types, and a value-typed view tree. Once the substrate is visible, the parts of SwiftUI that surprise developers (AnyView, Group, ViewBuilder, @ViewBuilder parameters, the dreaded some View vs any View error) stop being mysterious.
A SwiftUI view is a value type that conforms to a single protocol with a single requirement. The rest of the framework is built on Swift language features that exist outside SwiftUI: result builders, opaque types, generics with constraints, property wrappers. If you understand the language features, the framework reads like a normal Swift API. If you do not, the framework reads like magic that occasionally bites.
The post walks through the substrate. There is no LiveActivityManager here, no Get Bananas screenshot. The point is the framework, not a project; once the framework is legible, every shipped-code post in the cluster reads cleaner.
TL;DR
- A SwiftUI view is a Swift value type conforming to
View. The protocol has one requirement:var body: some View { get }. Everything else is built on top of Swift language features. @ViewBuilderis a result builder. The body of everyViewis one. Result builders turn comma-separated expressions into a single return value through compiler-synthesized calls.some Viewis an opaque return type. The compiler knows the concrete type; the caller does not. The opaque type is what makes view bodies fast at compile and runtime;AnyViewis the type-erased escape hatch for cases where opacity does not work.Group,EmptyView,TupleView,_ConditionalContentare the implementation types result builders synthesize. They are documented but rarely written by hand.
The Protocol That Starts Everything
The View protocol has one requirement:1
public protocol View {
associatedtype Body : View
@ViewBuilder var body: Self.Body { get }
}
Two parts of that protocol matter for understanding the rest of SwiftUI.
The associated type Body : View. A view’s body is itself a view. The recursion is what makes the framework composable. Every View returns another View from its body, and so on, until the chain terminates at one of the framework’s primitive views (such as Text, Color, Image, EmptyView) whose Body is Never. Primitive views are the leaves of the tree; views you write are the branches.
The @ViewBuilder attribute on body. Every body is a result-builder closure. Result builders are a Swift language feature documented in SE-0289 (formalized as @resultBuilder in Swift 5.4) that lets a closure with a sequence of expressions be transformed by the compiler into a single return value through synthesized method calls.2 The transformation is what makes the comma-free, statement-shaped syntax inside a SwiftUI body work.
The protocol shape is unusual for two reasons.
First, the requirement is a computed property, not a method. The view’s body is recomputed on every render pass when SwiftUI decides the view’s state has changed. The framework treats body as cheap to call; long computations inside body are an anti-pattern because they run on every render.
Second, Self.Body is associated, not erased. A view’s concrete body type is part of its signature at compile time. Text("Hello")’s body type is Never; a custom view’s body type is whatever @ViewBuilder synthesized for the body. The associated-type design is what lets the compiler optimize the view tree without runtime type checks. It is also what creates the some View requirement when a custom view returns conditional content.
Result Builders: The Comma-Free DSL
A result builder is a Swift language feature that transforms a closure into a single return value by inserting compiler-synthesized method calls. @ViewBuilder is a result builder. The body of every SwiftUI view is its closure.2
Consider this view:
struct ExampleView: View {
var body: some View {
Text("Title")
Text("Subtitle")
Image(systemName: "star")
}
}
The body has three statements with no separator. In normal Swift, that is a compiler error: a closure can return only one value. Result builders rewrite the closure before compilation. The actual code the compiler sees, after @ViewBuilder’s expansion, is roughly:
struct ExampleView: View {
var body: some View {
ViewBuilder.buildBlock(
Text("Title"),
Text("Subtitle"),
Image(systemName: "star")
)
}
}
ViewBuilder.buildBlock(_:_:_:) is a static method that takes three views and returns a TupleView<(Text, Text, Image)>. The body returns that single tuple-view value. Older SwiftUI shipped a fixed set of buildBlock overloads for 1, 2, 3, … up to 10 children; current SwiftUI uses Swift’s variadic-generics support (buildBlock<each Content>) so a body with eleven or more sibling views is no longer a special case.
The same pattern handles control flow. A view body with an if statement looks like this:
struct ConditionalView: View {
let isActive: Bool
var body: some View {
if isActive {
Text("Active")
} else {
Text("Inactive")
}
}
}
@ViewBuilder rewrites it through buildEither(first:) / buildEither(second:) calls, producing a _ConditionalContent<Text, Text>. The compiler knows the result type at compile time, even though only one branch runs at any given render.
if let, switch, optional unwrapping, and a few other constructs are handled by the result builder’s various buildXxx static methods.3 Repeated content is the one notable case the language feature supports through buildArray but @ViewBuilder does not: a raw for loop inside a body fails with “closure containing control flow statement cannot be used with result builder ‘ViewBuilder’.” The SwiftUI-shaped answer is ForEach, which takes a RandomAccessCollection and a content closure and synthesizes the iteration as a single value-typed view. The DSL is not bespoke; it is Swift result builders, configured for views.
some View And The Opacity Problem
A custom view’s body usually returns some View. The keyword is opaque return type and was added in Swift 5.1.4
some View says: “I return a specific type that conforms to View, but I’m not telling you which one.” The compiler tracks the concrete type internally for optimization; the caller of your view sees only the protocol witness. The pattern is what lets a view’s body return a complex type like VStack<TupleView<(Text, Image, Spacer)>> without requiring you to write that type in your source code.
Two things about some View that confuse new SwiftUI developers:
some View is one specific type, even when you return different things. The expression if condition { Text("A") } else { Image("b") } is allowed inside a @ViewBuilder body because the result builder wraps both branches in _ConditionalContent, producing a single concrete type. But the expression if condition { return Text("A") } else { return Image("b") } outside a result builder is a compiler error: the two branches return different concrete types, and some View requires one. Result builders are what make conditional return shapes work; explicit returns lose the result-builder transformation.
some View is not the same as any View. some View is opaque (one specific type, hidden); any View is existential (a box that can hold any conforming type, with runtime overhead). A free function or property can legally return any View. The View protocol’s body, however, cannot: the protocol requires associatedtype Body: View, and any View does not itself conform to View, so var body: any View fails to satisfy the protocol and the compiler suggests some View. The practical rule: use some View for view bodies, reach for AnyView (the type-erased wrapper) for runtime-variant view types. The error message “function declares an opaque return type but the return statements in its body do not have matching underlying types” almost always means you tried to return different concrete types from a some View function and you need either result-builder branching or AnyView.
AnyView: The Escape Hatch
AnyView is a type-erased view wrapper. Construction is AnyView(myView). The wrapper holds any conforming view, and SwiftUI accepts it where a View is expected.5
The escape-hatch use case is a function that returns different concrete types based on runtime data and that cannot be expressed through result-builder branching:
func viewForKind(_ kind: Kind) -> AnyView {
switch kind {
case .text: return AnyView(Text("hello"))
case .image: return AnyView(Image("photo"))
case .custom: return AnyView(MyCustomView())
}
}
The cost of AnyView is that the underlying type is not part of the view’s static identity. Apple’s documentation describes the consequence directly: when the type wrapped inside an AnyView changes across renders, the existing view hierarchy is destroyed and a new hierarchy is created in its place, which means lost state, restarted animations, and lost identity. Re-wrapping the same concrete type does not trigger that destruction, but the static-type-driven diffing the framework prefers is no longer available either way.
The right rule is: prefer @ViewBuilder result-builder branching for conditional views (if, switch, for), prefer parameterized views for varying types, reach for AnyView only when neither works. A func viewForKind that returns AnyView is usually a sign you should make viewForKind return some View and put a switch inside a result-builder closure.
Group, EmptyView, TupleView: The Implementation Types
The result builder synthesizes specific concrete view types. Three of them are useful to recognize:6
Group is a transparent container. It accepts up to ten views as content and presents them as siblings to the parent layout. The container itself adds no visual structure; the contents render exactly as they would individually. The use case is wrapping multiple views in a context that expects a single view (a .if modifier, a conditional return, a function that produces “one view”). Group { Text("A"); Text("B") } is a single view containing two; it is the explicit form of what result builders do implicitly.
EmptyView is a view that renders nothing. The result builder uses it as the conditional false branch when an if has no else. Returning EmptyView() from your own code is a way to opt out of rendering without changing the function’s return type.
TupleView is the concrete type result builders produce when a body has multiple sibling views. The expression at the top of this post that returns three sibling views actually returns a TupleView<(Text, Text, Image)>. You almost never write TupleView directly; you read it in error messages.
_ConditionalContent (with the leading underscore) is the type that handles if/else branches. The type appears in ViewBuilder’s public surface, but the underscored name signals “do not write against this casually”; let the result builder synthesize it from if/else rather than constructing it by hand.
@ViewBuilder On Your Own Functions
Result builders are not just for body. Any function or closure parameter can be annotated @ViewBuilder, and the same DSL syntax becomes legal inside it.2
struct Card<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
content
}
.padding()
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
// Usage: callers get the result-builder DSL inside the closure.
Card {
Text("Title")
Text("Subtitle")
Image(systemName: "star")
}
The pattern is how SwiftUI’s own VStack, HStack, ZStack, List, Form, Section, Group, and NavigationStack accept multiple children. Every one of those types takes a @ViewBuilder content: () -> Content parameter. Recognizing the pattern means you can write your own container views with the same ergonomics as the framework’s, no special compiler support required.
The reason you write init(@ViewBuilder content:) and not just init(content:) is that the attribute on the parameter is what activates the result-builder transformation inside the closure body the caller passes. Without the attribute, Card { Text("A"); Text("B") } is a compiler error because the closure has two statements and no @ViewBuilder to transform them.
State, Bindings, And The Property Wrapper Layer
Everything above is about the view tree’s shape. The other half of SwiftUI is state, and that half is built on Swift property wrappers.7
The property wrappers most relevant to view authoring:
@State owns a piece of value-type state inside a single view. Reading the property reads the underlying storage; assigning to it triggers a view re-render. The wrapper is appropriate for simple, view-local state (a toggle’s on/off, a text field’s draft string).
@Binding is a two-way reference into another view’s state. A child view that needs to read and write a parent’s state takes a Binding<T> parameter. The parent constructs the binding through $state (the dollar-sign projection on @State).
@Observable (iOS 17+) is a macro that replaces the older ObservableObject conformance pattern. The macro applied to a class generates Observation-framework tracking so that properties of the class trigger view re-renders when read inside a body and later changed. View-side ownership of an @Observable class moves from @StateObject to plain @State; downstream views that need a two-way handle use @Bindable instead of @ObservedObject.
@Environment reads dependency-injected values out of the environment chain. SwiftUI provides built-in environment keys (locale, color scheme, dismiss action); apps add custom keys for domain-specific dependency injection.
The property-wrapper layer is what lets a view’s body re-execute when state changes. SwiftUI tracks reads inside body through two distinct mechanisms: AttributeGraph (Apple’s private dependency graph that backs @State, @Binding, and @Environment) for the older property-wrapper path, and the standard library’s Observation framework (withObservationTracking, public on iOS 17+) for @Observable types.8 When a tracked property is mutated, the corresponding bodies are re-executed and the diffing machinery computes the minimum view-tree change.
The two halves (the view-tree layer and the state layer) are loosely coupled. The view tree is value-typed and fast to recompute. The state layer is reference-typed (for @Observable) or value-typed-with-storage-pointer (for @State) and tracks reads. Together they produce the framework’s “describe what should be on screen as a function of state, and the framework figures out the diff” model.
What You Now Recognize In Error Messages
Reading SwiftUI compiler errors with the substrate visible:
“Function declares an opaque return type, but the return statements in its body do not have matching underlying types.” Two return statements with different concrete types in a some View function. Fix: use @ViewBuilder so the result builder wraps both in _ConditionalContent, or wrap both returns in AnyView.
“The compiler is unable to type-check this expression in reasonable time.” Long body chains with many modifiers exhaust the type checker. Fix: break the body into smaller computed properties or sub-views; each some View-returning piece simplifies the inference work.
“Cannot convert value of type ‘TupleView<…>’ to expected type ‘some View’.” A function expecting one view received the result of a multi-statement body without @ViewBuilder. Fix: add @ViewBuilder to the closure parameter accepting the multi-statement content.
“Generic parameter ‘Content’ could not be inferred.” A custom container takes @ViewBuilder content: () -> Content and the call site has an empty closure. Fix: result builders need at least one expression to infer Content; empty closures fall back to EmptyView() if the call site explicitly provides it.
The error messages are unfriendly because the substrate is invisible. Reading them with the substrate visible turns most of them into “ah, the result builder cannot transform this” or “ah, I need either branching or AnyView.”
When To Reach Outside The Substrate
A few patterns the substrate does not handle cleanly:
Variadic concrete types. A function that returns a different View type per branch and that you cannot wrap in result-builder branching needs AnyView. Accept the cost (lost diffing, no animation) and document the call site.
Cross-platform conditional views. Compile-time #if os(iOS) works inside a @ViewBuilder body but limits the result builder’s branching count; multi-OS conditional bodies sometimes hit the “expression too complex” limit. The fix is to extract per-platform sub-views into separate functions, each returning some View.
Imperative view construction. The framework expects views to be expressions, not constructed-then-mutated objects. UIKit-style “create the label, set the text, add to subview” does not translate; the SwiftUI equivalent is a value-typed Text("...") returned from a body. Patterns that require imperative construction are usually a sign the work belongs in a UIViewRepresentable bridge to UIKit.
What The Pattern Means For Apps Shipping On iOS 26+
Three takeaways.
-
SwiftUI is Swift, not magic. Result builders, opaque return types, and property wrappers are all in the Swift language reference. Reading the framework as Swift code, not as a special DSL, makes the surprising parts predictable.
-
some ViewandAnyViewsolve different problems. Opaque return types are the default; type erasure is the escape hatch. Reaching forAnyViewshould be the rare case; reaching forsome Viewplus result-builder branching should be the common one. -
Result builders are the whole DSL. Anywhere a function or parameter is
@ViewBuilder, the comma-free, statement-shaped syntax is available. Writing your own container views with the same ergonomics asVStackis one attribute and one closure parameter.
Pair this post with the cluster’s shipped-code series: cross-platform SwiftUI shipping (Return runs on five platforms with one shared SwiftUI core); the Liquid Glass visual layer; the Live Activities state machine on iOS; the watchOS runtime contract on Apple Watch. The hub is at the Apple Ecosystem Series. For broader iOS-with-AI-agents context, see the iOS Agent Development guide.
FAQ
What is the View protocol in SwiftUI?
The View protocol has one requirement: var body: some View { get }. Every SwiftUI view is a Swift value type conforming to View, with a body computed property that returns another view (or Never for primitive views like Text, Color, Image, EmptyView). The body is annotated @ViewBuilder so it can use SwiftUI’s comma-free DSL syntax.
What does some View mean?
some View is an opaque return type (Swift 5.1+). The compiler knows the concrete type; the caller sees only the protocol witness. Opaque types let view bodies return complex types like VStack<TupleView<(Text, Image, Spacer)>> without spelling them out, while preserving compile-time optimization. some View is one specific type, even though that type is not visible at the call site.
When should I use AnyView?
Use AnyView only when neither @ViewBuilder result-builder branching (if, switch, for) nor parameterized generics solve the problem. When the wrapped concrete type changes across renders, the existing view hierarchy is destroyed and a new one is created in its place; that is the moment animations restart and view state resets. Re-wrapping the same concrete type does not trigger that destruction, but the static-type-driven diffing the framework prefers is no longer available either way. If you find yourself reaching for AnyView often, the pattern that needs to change is upstream: prefer parameterized views or push the conditional into a result-builder body.
What’s @ViewBuilder and where can I use it?
@ViewBuilder is a result builder (Swift language feature). It transforms a closure with multiple expressions into a single return value by inserting compiler-synthesized buildBlock, buildEither, buildOptional, etc. calls. The body of every SwiftUI view is @ViewBuilder by default. You can apply @ViewBuilder to any function or closure parameter to give callers the same DSL syntax; VStack, Card, and Section use the same pattern to accept multiple children.
Why does my view body re-render when I didn’t expect it to?
SwiftUI re-runs body whenever any state property the body reads is mutated. Property wrappers (@State, @Binding, @Observable, @Environment) track reads and trigger re-renders on writes. Unexpected re-renders usually trace to a parent view’s state changing, an environment value changing, or an @Observable object’s read property being modified. The framework’s diffing then computes the minimum tree change.
References
-
Apple Developer, “View” and “Configuring views”. The
Viewprotocol,Bodyassociated type, and the@ViewBuilderattribute onbody. ↩ -
Swift Evolution, “SE-0289: Result builders”. The language proposal that formalized result builders (introduced as
_functionBuilderin 5.1, formalized as@resultBuilderin 5.4). DefinesbuildBlock,buildEither,buildOptional,buildArray,buildExpression,buildFinalResult, and friends. ↩↩↩ -
Apple Developer, “ViewBuilder” and “ForEach”. The result-builder type SwiftUI uses for view bodies (variadic-generic
buildBlock,buildEither, optional unwrapping).ViewBuilderdoes not exposebuildArray, soForEachis the iteration primitive for repeating a view over a collection. ↩ -
Swift Evolution, “SE-0244: Opaque result types”. The
somekeyword for opaque return types, added in Swift 5.1. ↩ -
Apple Developer, “AnyView”. Type-erased view wrapper, construction, and the diffing trade-off. ↩
-
Apple Developer, “Group”, “EmptyView”, and “TupleView”. Implementation types that result builders synthesize. ↩
-
Apple Developer, “State and Data Flow”. The property-wrapper layer:
@State,@Binding,@Observable,@Environment. SwiftUI’s observation system and the iOS 17+@Observablemacro. ↩ -
Apple Developer, “Observation” and “Migrating from the Observable Object protocol to the Observable macro”. The standard-library Observation framework, including
withObservationTracking(_:onChange:), plus the iOS 17 migration path fromObservableObjectto@Observable. ↩