DEV Community

Breaking Up Big SwiftUI Views the Right Way

Why computed properties help readability, what @ViewBuilder actually does, and when a separate View type matters Every SwiftUI codebase eventually grows a view nobody wants to open. Four hundred lines. A body that scrolls forever. Several layers of modifiers. State, navigation, sheets, animations, and business logic all living in the same place. The usual first attempt at cleaning it up is to split body into computed properties: var body: some View { VStack { header content footer } } private var header: some View { // ... } private var content: some View { // ... } private var footer: some View { // ... } That makes the file easier to read, and that's useful. But it is important to understand what this refactoring does and doesn't change. A computed property doesn't create a new View type, a new identity, or an independent dependency-tracking node. It is still part of the same enclosing view's body evaluation. If you're trying to improve update locality or isolate expensive sections of UI, that's where extracting a separate View type becomes useful. The important distinction is: Computed properties organize your source code. Separate View types can organize your view hierarchy, dependencies, state, and update work. Let's look at why. The mental model that makes SwiftUI easier to reason about SwiftUI is easier to understand if you think in terms of three concepts: - Identity - how SwiftUI recognizes something as the same or different across updates. - Lifetime - how SwiftUI associates state with that identity. - Dependencies - which pieces of data a view reads and therefore depends on. Apple describes these concepts as fundamental to how SwiftUI decides what needs to change and when. The important one for this discussion is dependencies. Consider: struct UserNameView: View { let user: User var body: some View { Text(user.name) } } With SwiftUI's Observation system, the view establishes a dependency on user.name because its body reads that property. If user.email changes but body doesn't read email , that change doesn't create the same dependency. This is why it's better to think: "What data does this view depend on?" rather than: "Which giant view contains this code?" Apple's documentation explains that Observation tracks the observable properties actually read during a view's body evaluation. That distinction becomes important when deciding whether a large view should be split. A computed property is not a separate view Consider this: struct DashboardView: View { @State private var count = 0 var body: some View { VStack { Button("Count: (count)") { count += 1 } expensiveSection } } private var expensiveSection: some View { ForEach(0.. some View { ForEach(0.. ( _ condition: Bool, transform: (Self) -> Content ) -> some View { if condition { transform(self) } else { self } } } You might then write: Rectangle() .applyIf(isCompact) { $0.frame(width: 100) } It looks convenient. But the if creates structurally different view branches. When you are actually trying to modify the same logical view, it's usually better to express the condition as a modifier parameter: Rectangle() .frame(width: isCompact ? 100 : nil) The second version keeps the operation on the same logical view rather than conditionally producing two different structures. This matters for identity, state lifetime, transitions, and animations. But if is not bad This distinction is important. Don't take the previous section to mean: "Never use if in SwiftUI." That's completely wrong. This is perfectly normal: if isLoading { ProgressView() } else { ContentView() } Those are genuinely different pieces of UI. Conditional structure is exactly what @ViewBuilder is designed to express. The problem is using structural branching to conditionally apply a modifier to what is conceptually the same view. A useful rule is: Use structural branching when the content is actually different. Prefer conditional modifier parameters when the content is the same and only a property changes. State belongs where it is used Another benefit of extracting views is clearer state ownership. Instead of: struct DashboardView: View { @State private var isChartExpanded = false @State private var selectedRange: DateRange = .week @State private var showDetails = false // hundreds of lines... } you can move state toward the component that owns it: struct DashboardView: View { @State private var selectedRange: DateRange = .week var body: some View { VStack { ChartSection() RangeControls(selection: $selectedRange) } } } struct ChartSection: View { @State private var isExpanded = false var body: some View { // ... } } Now the state has a clear owner. This also makes the lifetime of that state easier to reason about because SwiftUI associates state with the identity of the view that owns it. That's one reason structural identity matters. A useful code-review checklist When reviewing a large SwiftUI view, ask these questions. 1. Is this computed property only improving readability? If yes, that's fine. Don't extract it solely because someone says every some View property should become a struct. 2. Does this section have its own state? If yes, consider making it a separate View . 3. Does this section have a distinct dependency surface? If it only needs: let title: String let count: Int let isEnabled: Bool that's often a good component boundary. 4. Am I passing an entire model when the component only needs a few values? Prefer narrow inputs when practical. But remember that with the Observation framework, passing an observable reference does not automatically mean the view depends on every property of that object. Dependencies are established by the properties read by body . 5. Am I doing expensive work during body evaluation? If yes, ask whether that work should be: - moved, - cached, - simplified, - performed asynchronously, - or otherwise optimized. Extracting a view can improve structure, but it doesn't automatically make expensive algorithms cheap. 6. Am I using @ViewBuilder because I need conditional structure? Good. Am I using it because I believe it creates a performance boundary? That's the wrong reason. 7. Am I conditionally applying a modifier with if ? Ask whether the modifier can instead take a conditional parameter: .padding(isCompact ? 8 : 16) rather than creating separate structural branches unnecessarily. 8. Does this component have a meaningful responsibility? If yes, a separate type may improve the architecture. If not, a computed property may be clearer. What about performance? This is the most important qualification. Don't turn view extraction into a cargo-cult performance rule. This: struct SmallView: View { var body: some View { Text("Hello") } } isn't automatically faster than: private var smallView: some View { Text("Hello") } And making a view smaller doesn't automatically make the application faster. The value of extraction comes from better separation of dependencies, state, identity, and work. If you have a performance problem, measure it. Useful things to investigate include: - How often is a view's body being evaluated? - Which state changes cause the evaluation? - Is expensive computation happening during body evaluation? - Are large collections being transformed repeatedly? - Is layout expensive? - Is drawing expensive? - Is an animation causing frequent updates? - Is state owned at the wrong level? - Are observable dependencies broader than necessary? Use Instruments and SwiftUI's debugging/profiling facilities rather than assuming that a refactoring is faster because it looks more modular. The rule of thumb Here's the mental model I use: Computed properties are for organizing a view's implementation. @ViewBuilder is for expressing view structure and conditional composition.Separate View types are for creating meaningful components with their own identity, state ownership, dependencies, and responsibilities.Narrow inputs make those boundaries easier to reason about. Performance improvements should be measured rather than assumed. That's a much more reliable way to structure SwiftUI code. The practical takeaway When you encounter a 500-line SwiftUI view, don't immediately turn every computed property into another struct. Instead, look for meaningful boundaries. Start with sections that: - own local state, - have a distinct set of dependencies, - perform meaningful computation, - contain complex UI, - are reused, - need independent previews, - or are updated independently from the rest of the screen. For example: struct DashboardView: View { @State private var model = DashboardModel() var body: some View { DashboardContent( summary: SummaryData( distance: model.totalDistance, streak: model.currentStreak ), samples: model.samples ) } } Then let the components own their own concerns: struct DashboardContent: View { let summary: SummaryData let samples: [Sample] var body: some View { VStack { SummarySection(data: summary) ChartSection(samples: samples) } } } And keep small structural branches local: @ViewBuilder private var connectionStatus: some View { switch status { case .offline: OfflineBadge() case .syncing: ProgressView() case .connected: ConnectedBadge() } } That's a good balance. You get readable code without turning the entire application into hundreds of tiny view types. Final thought Breaking up a large SwiftUI view isn't about hitting a magic number of lines. It's about making the structure of the UI reflect the structure of its responsibilities. A computed property can make a large view easier to read. A @ViewBuilder can make conditional composition easier to express. A separate View type can give a piece of UI a clearer identity, state owner, dependency surface, and responsibility. And when performance is the motivation, the most important question isn't: "Did I extract this into a struct?" It's: "Did I reduce unnecessary dependencies or expensive work, and can I measure the difference?" That's the approach that scales. Top comments (0)

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.