Series · Become a better iOS engineer
Splitting SwiftUI views: computed properties vs real View structs
Your body got long, so you broke it into a few private var computed
properties. It reads better now. But if you did it expecting a performance
win, nothing changed — and understanding why teaches you how SwiftUI actually
decides what to re-evaluate.
First principle: what triggers a re-evaluation
A SwiftUI view’s body is recomputed whenever one of the dependencies it
reads changes — a @State, a @Binding, an @Observable property, an
@Environment value. SwiftUI tracks those reads, and when one of them
invalidates, it calls body again.
The unit of that tracking is the View struct. Each struct has one
body, and SwiftUI draws an invalidation boundary around it: “when my inputs
change, re-run this body.”
Why computed properties change nothing
struct ProfileView: View {
@State private var isExpanded = false
let name: String
var body: some View {
VStack {
header
details
}
}
private var header: some View { Text(name).font(.title) }
private var details: some View { /* ...expensive... */ }
}
header and details look like sub-views, but they aren’t. They’re just
functions that get called inside ProfileView.body. They live within the
same invalidation boundary. So when isExpanded flips, body re-runs — and
that re-runs header and details, every time, even though header
only depends on name, which didn’t change.
Splitting into computed properties is a readability tool. It is not a performance tool.
What a real View struct buys you
var body: some View {
VStack {
ProfileHeader(name: name)
ProfileDetails(isExpanded: isExpanded)
}
}
Now ProfileHeader and ProfileDetails are their own structs, each with its
own boundary. SwiftUI compares the inputs it passed in: when isExpanded
changes, ProfileHeader’s only input (name) is unchanged, so SwiftUI can
skip re-evaluating its body entirely. Only ProfileDetails re-runs.
The boundary moved. That’s the whole difference: a computed property shares the parent’s boundary; a struct creates a new one.
The senior caveat: don’t cargo-cult it
Re-evaluating a body is not the same as re-rendering pixels, and for cheap
views it costs almost nothing. Extracting every subview “for performance” adds
its own noise and can hurt readability for zero benefit.
The rule I actually apply:
- Split into computed properties freely — for readability.
- Extract into a
Viewstruct when a subtree is expensive and its inputs change on a different cadence than its siblings. - Then measure, don’t guess.
Self._printChanges()inside a body prints exactly what caused each re-evaluation; Instruments’ SwiftUI template shows the cost.
Know the boundary, and you stop optimizing by superstition.