Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Reviews, improves, and writes SwiftUI code following state management, view composition, performance, and iOS 26+ Liquid Glass best practices.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/view-structure.md
1# SwiftUI View Structure Reference23## Table of Contents45- [View Structure Principles](#view-structure-principles)6- [Recommended View File Structure](#recommended-view-file-structure)7- [Struct or Method / Computed Property?](#struct-or-method--computed-property)8- [Prefer Modifiers Over Conditional Views](#prefer-modifiers-over-conditional-views)9- [Extract Subviews, Not Computed Properties](#extract-subviews-not-computed-properties)10- [@ViewBuilder](#viewbuilder)11- [Keep View Body Simple and Avoid High-Cost Operations](#keep-view-body-simple-and-avoid-high-cost-operations)12- [When to Extract Subviews](#when-to-extract-subviews)13- [Container View Pattern](#container-view-pattern)14- [Utilize Lazy Containers for Large Data Sets](#utilize-lazy-containers-for-large-data-sets)15- [ZStack vs overlay/background](#zstack-vs-overlaybackground)16- [Compositing Group Before Clipping](#compositing-group-before-clipping)17- [Split State-Driven Parts into Custom View Types](#split-state-driven-parts-into-custom-view-types)18- [Reusable Styling with ViewModifier](#reusable-styling-with-viewmodifier)19- [Skeleton Loading with Redacted Views](#skeleton-loading-with-redacted-views)20- [AnyView](#anyview)21- [UIViewRepresentable Essentials](#uiviewrepresentable-essentials)22- [Troubleshooting](#troubleshooting)23- [Summary Checklist](#summary-checklist)2425## View Structure Principles2627SwiftUI's diffing algorithm compares view hierarchies to determine what needs updating. Proper view composition directly impacts performance.2829## Recommended View File Structure3031Use a consistent order when declaring SwiftUI views:32331. Environment Properties342. State Properties353. Private Properties364. Initializer (if needed)375. Body386. Computed Properties/Methods for Subviews3940```swift41struct ContentView: View {42// MARK: - Environment Properties43@Environment(\.colorScheme) var colorScheme4445// MARK: - State Properties46@Binding var isToggled: Bool47@State private var viewModel: SomeViewModel4849// MARK: - Private Properties50private let title: String = "SwiftUI Guide"5152// MARK: - Initializer (if needed)53init(isToggled: Binding<Bool>) {54self._isToggled = isToggled55}5657// MARK: - Body58var body: some View {59VStack {60header61content62}63}6465// MARK: - Computed Subviews66private var header: some View {67Text(title).font(.largeTitle).padding()68}6970private var content: some View {71VStack {72Text("Counter: \(counter)")73}74}75}76```7778## Struct or Method / Computed Property?7980If a `View` is intended to be reusable across multiple screens, encapsulate it within a separate `struct`. If its usage is confined to a single context, it can be declared as a function or computed property within the containing `View`.8182However, if a view maintains state using `@State`, `@Binding`, `@ObservedObject`, `@Environment`, `@StateObject`, or similar wrappers, it should generally be a separate `struct`.8384- For simple, static views: a computed property is acceptable.85- For views requiring parameters: a method is more appropriate, but only when those parameters are stable. If parameters change per-call (e.g. inside a `ForEach` where each call receives a different item), prefer a separate `struct` so SwiftUI can diff inputs and skip body evaluation.86- For reusable, stateful, or logically independent UI sections: prefer a dedicated `struct`.8788```swift89struct ContentView: View {90var titleView: some View {91Text("Hello from Property")92.font(.largeTitle)93.foregroundColor(.blue)94}9596func messageView(text: String, color: Color) -> some View {97Text(text)98.font(.title)99.foregroundColor(color)100.padding()101}102103var body: some View {104VStack {105titleView106messageView(text: "Hello from Method", color: .red)107}108}109}110```111112## Prefer Modifiers Over Conditional Views113114**Prefer "no-effect" modifiers over conditionally including views.** When you introduce a branch, consider whether you're representing multiple views or two states of the same view.115116### Use Opacity Instead of Conditional Inclusion117118```swift119// Good - same view, different states120SomeView()121.opacity(isVisible ? 1 : 0)122123// Avoid - creates/destroys view identity124if isVisible {125SomeView()126}127```128129**Why**: Conditional view inclusion can cause loss of state, poor animation performance, and breaks view identity. Using modifiers maintains view identity across state changes.130131### When Conditionals Are Appropriate132133Use conditionals when you truly have **different views**, not different states:134135```swift136// Correct - fundamentally different views137if isLoggedIn {138DashboardView()139} else {140LoginView()141}142143// Correct - optional content144if let user {145UserProfileView(user: user)146}147```148149### Conditional View Modifier Extensions Break Identity150151A common pattern is an `if`-based `View` extension for conditional modifiers. This changes the view's return type between branches, which destroys view identity and breaks animations:152153```swift154// Problematic -- different return types per branch155extension View {156@ViewBuilder func `if`<T: View>(_ condition: Bool, transform: (Self) -> T) -> some View {157if condition {158transform(self) // Returns T159} else {160self // Returns Self161}162}163}164```165166Prefer applying the modifier directly with a ternary or always-present modifier:167168```swift169// Good -- same view identity maintained170Text("Hello")171.opacity(isHighlighted ? 1 : 0.5)172173// Good -- modifier always present, value changes174Text("Hello")175.foregroundStyle(isError ? .red : .primary)176```177178## Extract Subviews, Not Computed Properties179180### The Problem with @ViewBuilder Functions181182When you use `@ViewBuilder` functions or computed properties for complex views, the entire function re-executes on every parent state change:183184```swift185// BAD - re-executes complexSection() on every tap186struct ParentView: View {187@State private var count = 0188189var body: some View {190VStack {191Button("Tap: \(count)") { count += 1 }192complexSection() // Re-executes every tap!193}194}195196@ViewBuilder197func complexSection() -> some View {198// Complex views that re-execute unnecessarily199ForEach(0..<100) { i in200HStack {201Image(systemName: "star")202Text("Item \(i)")203Spacer()204Text("Detail")205}206}207}208}209```210211### The Solution: Separate Structs212213Extract to separate `struct` views. SwiftUI can skip their `body` when inputs don't change:214215```swift216// GOOD - ComplexSection body SKIPPED when its inputs don't change217struct ParentView: View {218@State private var count = 0219220var body: some View {221VStack {222Button("Tap: \(count)") { count += 1 }223ComplexSection() // Body skipped during re-evaluation224}225}226}227228struct ComplexSection: View {229var body: some View {230ForEach(0..<100) { i in231HStack {232Image(systemName: "star")233Text("Item \(i)")234Spacer()235Text("Detail")236}237}238}239}240```241242### Why This Works2432441. SwiftUI compares the `ComplexSection` struct (which has no properties)2452. Since nothing changed, SwiftUI skips calling `ComplexSection.body`2463. The complex view code never executes unnecessarily247248## @ViewBuilder249250Use `@ViewBuilder` functions for small, simple sections (a few views, no expensive computation) that don't affect performance. They work particularly well for static content that doesn't depend on any `@State` or `@Binding`, since SwiftUI won't need to diff them independently. Extract to a separate `struct` when the section is complex, depends on state, or needs to be skipped during re-evaluation.251252The `@ViewBuilder` attribute is only required when a function or computed property returns multiple different views conditionally, for example through `if` or `switch`:253254```swift255@ViewBuilder256private var conditionalView: some View {257if isExpanded {258VStack {259Text("Expanded View")260Image(systemName: "star")261}262} else {263Text("Collapsed View")264}265}266```267268If every branch returns the same concrete type, `@ViewBuilder` is unnecessary:269270```swift271var conditionalText: some View {272if Bool.random() {273Text("Hello")274} else {275Text("World")276}277}278```279280Prefer `@ViewBuilder` when:281282- there is conditional branching between multiple view types283- extracting a separate `struct` would not provide meaningful separation284285## Keep View Body Simple and Avoid High-Cost Operations286287Refrain from performing complex operations within the `body` of your view. Instead of passing a ready-to-use sequence with filtering, mapping, or sorting directly into `ForEach`, prepare the sequence outside the body.288289```swift290// Avoid such things ...291var body: some View {292List {293ForEach(model.values.filter { $0 > 0 }, id: \.self) {294Text(String($0))295.padding()296}297}298}299```300301Prefer:302303```swift304struct FilteredListView: View {305private let filteredValues: [Int]306307init(values: [Int]) {308self.filteredValues = values.filter { $0 > 0 } // Perform filtering once309}310311var body: some View {312List {313content314}315}316317private var content: some View {318ForEach(filteredValues, id: \.self) { value in319Text(String(value))320.padding()321}322}323}324```325326The reason this matters is that the system can call `body` multiple times during a single layout phase. Complex body computation makes those calls more expensive than necessary.327328General guidance:329330- avoid filtering, sorting, and mapping inline in `body`331- avoid constructing expensive formatters in `body`332- avoid heavy branching in large view trees333- move data preparation into init, model layer, or dedicated helpers334335## When to Extract Subviews336337Extract complex views into separate subviews when:338- The view has multiple logical sections or responsibilities339- The view contains reusable components340- The view body becomes difficult to read or understand341- You need to isolate state changes for performance342- The view is becoming large (keep views small for better performance)343- The section may evolve independently over time344345## Container View Pattern346347### Avoid Closure-Based Content348349Closures can't be compared, causing unnecessary re-renders:350351```swift352// BAD - closure prevents SwiftUI from skipping updates353struct MyContainer<Content: View>: View {354let content: () -> Content355356var body: some View {357VStack {358Text("Header")359content() // Always called, can't compare closures360}361}362}363364// Usage forces re-render on every parent update365MyContainer {366ExpensiveView()367}368```369370### Use @ViewBuilder Property Instead371372```swift373// GOOD - view can be compared374struct MyContainer<Content: View>: View {375@ViewBuilder let content: Content376377var body: some View {378VStack {379Text("Header")380content // SwiftUI can compare and skip if unchanged381}382}383}384385// Usage - SwiftUI can diff ExpensiveView386MyContainer {387ExpensiveView()388}389```390391## Utilize Lazy Containers for Large Data Sets392393When displaying extensive lists or grids, prefer `LazyVStack`, `LazyHStack`, `LazyVGrid`, or `LazyHGrid`. These containers load views only when they appear on the screen, reducing memory usage and improving performance.394395```swift396struct ContentView: View {397let items = Array(0..<1000)398399var body: some View {400ScrollView {401LazyVStack {402ForEach(items, id: \.self) { item in403Text("Item \(item)")404}405}406}407}408}409```410411Prefer lazy containers when:412413- rendering large collections414- row views are non-trivial415- memory usage matters416- the content is inside `ScrollView`417418## ZStack vs overlay/background419420Use `ZStack` to **compose multiple peer views** that should be layered together and jointly define layout.421422Prefer `overlay` / `background` when you’re **decorating a primary view**.423Not primarily because they don’t affect layout size, but because they **express intent and improve readability**: the view being modified remains the clear layout anchor.424425A key difference is **size proposal behavior**:426- In `overlay` / `background`, the child view implicitly adopts the size proposed to the parent when it doesn’t define its own size, making decorative attachments feel natural and predictable.427- In `ZStack`, each child participates independently in layout, and no implicit size inheritance exists. This makes it better suited for peer composition, but less intuitive for simple decoration.428429Use `ZStack` (or another container) when the “decoration” **must explicitly participate in layout sizing**—for example, when reserving space, extending tappable/visible bounds, or preventing overlap with neighboring views.430431### Examples432433```swift434// GOOD - decoration via overlay (layout anchored to button)435Button("Continue") { }436.overlay(alignment: .trailing) {437Image(systemName: "lock.fill").padding(.trailing, 8)438}439440// BAD - ZStack when overlay suffices (layout no longer anchored to button)441ZStack(alignment: .trailing) {442Button("Continue") { }443Image(systemName: "lock.fill").padding(.trailing, 8)444}445446// GOOD - background shape takes parent size447HStack(spacing: 12) { Text("Inbox"); Text("Next") }448.background { Capsule().strokeBorder(.blue, lineWidth: 2) }449```450451## Compositing Group Before Clipping452453**Always add `.compositingGroup()` before `.clipShape()` when clipping layered views (`.overlay` or `.background`).** Without it, each layer is antialiased separately and then composited. Where antialiased edges overlap — typically at rounded corners — you get visible color fringes (semi-transparent pixels of different colors blending together).454455```swift456let shape = RoundedRectangle(cornerRadius: 16)457458// BAD - each layer antialiased separately, producing color fringes at corners459Color.red460.overlay(.white, in: shape)461.clipShape(shape)462.frame(width: 200, height: 150)463464// GOOD - layers composited first, antialiasing applied once during clipping465Color.red466.overlay(.white, in: .rect)467.compositingGroup()468.clipShape(shape)469.frame(width: 200, height: 150)470```471472`.compositingGroup()` forces all child layers to be rendered into a single offscreen buffer before the clip is applied. This means antialiasing only happens once — on the final composited result — eliminating the fringe artifacts.473474## Split State-Driven Parts into Custom View Types475476Large views often depend on multiple independent state sources. If a single view body depends on all of them, then any state change can cause the entire body to re-evaluate.477478```swift479struct BigAndComplicatedView: View {480@State private var counter = 0481@State private var isToggled = false482@StateObject private var viewModel = SomeViewModel()483484let title = "Big and Complicated View"485486var body: some View {487VStack {488Text(title)489.font(.largeTitle)490491Text("Counter: \(counter)")492.font(.title)493494Toggle("Enable Feature", isOn: $isToggled)495.padding()496497Button("Increment Counter") {498counter += 1499}500501Text("ViewModel Data: \(viewModel.data)")502.padding()503504Button("Fetch Data") {505viewModel.fetchData()506}507}508}509}510```511512### Better: Split Into Smaller Components513514```swift515struct BigAndComplicatedView: View {516@State private var counter = 0517@State private var isToggled = false518@StateObject private var viewModel = SomeViewModel()519520var body: some View {521VStack {522titleView523CounterView(counter: $counter)524ToggleView(isToggled: $isToggled)525ViewModelDataView(data: viewModel.data) {526viewModel.updateData()527}528.equatable()529}530}531532private var titleView: some View {533Text("Big and Complicated View")534.font(.largeTitle)535}536}537```538539Why this is better:540541- changing `counter` only affects `CounterView`542- toggling only affects `ToggleView`543- updating the model data only affects `ViewModelDataView`544545### Notes on Equatable546547Using `Equatable` for a view is not a universal best practice, but it can be useful in targeted cases where:548549- the input is small and well-defined550- the comparison logic is meaningful551- you want to reduce unnecessary body evaluation for a specific subtree552553Do not use `Equatable` as a blanket optimization technique.554555## Reusable Styling with ViewModifier556557Extract repeated modifier combinations into a `ViewModifier` struct. Expose via a `View` extension for autocompletion:558559```swift560private struct CardStyle: ViewModifier {561func body(content: Content) -> some View {562content563.padding()564.background(Color(.secondarySystemBackground))565.clipShape(.rect(cornerRadius: 12))566}567}568569extension View {570func cardStyle() -> some View {571modifier(CardStyle())572}573}574```575576### Custom ButtonStyle577578Use the `ButtonStyle` protocol for reusable button designs. Use `PrimitiveButtonStyle` only when you need custom interaction handling (e.g., simultaneous gestures):579580```swift581struct PrimaryButtonStyle: ButtonStyle {582func makeBody(configuration: Configuration) -> some View {583configuration.label584.bold()585.foregroundStyle(.white)586.padding(.horizontal, 16)587.padding(.vertical, 8)588.background(Color.accentColor)589.clipShape(Capsule())590.scaleEffect(configuration.isPressed ? 0.95 : 1)591.animation(.smooth, value: configuration.isPressed)592}593}594```595596### Discoverability with Static Member Lookup597598Make custom styles and modifiers discoverable via leading-dot syntax:599600```swift601extension ButtonStyle where Self == PrimaryButtonStyle {602static var primary: PrimaryButtonStyle { .init() }603}604605// Usage: .buttonStyle(.primary)606```607608This pattern works for any SwiftUI style protocol (`ButtonStyle`, `ListStyle`, `ToggleStyle`, etc.).609610## Skeleton Loading with Redacted Views611612Use `.redacted(reason: .placeholder)` to show skeleton views while data loads. Use `.unredacted()` to opt out specific views:613614```swift615VStack(alignment: .leading) {616Text(article?.title ?? String(repeating: "X", count: 20))617.font(.headline)618Text(article?.author ?? String(repeating: "X", count: 12))619.font(.subheadline)620Text("SwiftLee")621.font(.caption)622.unredacted()623}624.redacted(reason: article == nil ? .placeholder : [])625```626627Apply `.redacted` on a container to redact all children at once.628629## AnyView630631`AnyView` is type erasure. SwiftUI uses structural identity based on type information to determine when views should be updated.632633```swift634private var nameView: some View {635if isEditable {636TextField("Your name", text: $name)637} else {638Text(name)639}640}641```642643Avoid patterns like:644645```swift646private var nameView: some View {647if isEditable {648return AnyView(TextField("Your name", text: $name))649} else {650return AnyView(Text(name))651}652}653```654655Because `AnyView` erases type information, SwiftUI loses some optimization opportunities. Prefer `@ViewBuilder` or conditional branches with concrete view types.656657Use `AnyView` only when type erasure is truly necessary for API design.658659## UIViewRepresentable Essentials660661When bridging UIKit views into SwiftUI:662663- `makeUIView(context:)` is called **once** to create the UIKit view664- `updateUIView(_:context:)` is called on **every SwiftUI redraw** to sync state665- The representable struct itself is **recreated on every redraw** -- avoid heavy work in its init666- Use a `Coordinator` for delegate callbacks and two-way communication667668```swift669struct MapView: UIViewRepresentable {670let coordinate: CLLocationCoordinate2D671672func makeUIView(context: Context) -> MKMapView {673let map = MKMapView()674map.delegate = context.coordinator675return map676}677678func updateUIView(_ map: MKMapView, context: Context) {679map.setCenter(coordinate, animated: true)680}681682func makeCoordinator() -> Coordinator { Coordinator() }683684class Coordinator: NSObject, MKMapViewDelegate { }685}686```687688## Troubleshooting689690### Debug SwiftUI Renderings691692If it is needed to debug render cycles and read console output you can leverage the `_printChanges()` or `_logChanges()` methods on `View`. These methods print information about when the view is being evaluated and what changes are triggering updates. This can be very helpful when your view body is called multiple times and you want to know why.693694```swift695struct ContentView: View {696@State private var counter: Int = 99697698init() {699print(Self.self, #function)700}701702var body: some View {703let _ = Self._printChanges()704705VStack {706Text("Counter: \(counter)")707Button {708counter += 1709} label: {710Text("Counter +1")711}712.buttonStyle(.borderedProminent)713}714.padding()715}716}717```718719As an alternative to `Self._printChanges()`, you can use `_logChanges()`720721```swift722struct ContentView: View {723@State private var counter: Int = 99724725var body: some View {726let _ = Self._logChanges()727728VStack {729Text("Counter: \(counter)")730Button {731counter += 1732} label: {733Text("Counter +1")734}735.buttonStyle(.borderedProminent)736}737.padding()738}739}740```741742Use these tools only for debugging and remove them from production code.743744### Handling "The Compiler Is Unable to Type-Check This Expression in Reasonable Time"745746If you encounter:747748> The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions749750it is often caused by overly complex view structures or expressions.751752Ways to fix it:753754- break large expressions into smaller computed values755- extract subviews756- split long modifier chains757- simplify nested generics and builders758- avoid huge inline closures759760## Summary Checklist761762- [ ] Follow a consistent view file structure (Environment → State → Private → Init → Body → Subviews)763- [ ] Prefer modifiers over conditional views for state changes764- [ ] Avoid `if`-based conditional modifier extensions (they break view identity)765- [ ] Extract complex views into separate subviews, not computed properties766- [ ] Keep views small for readability and performance767- [ ] Use `@ViewBuilder` only where it actually adds value768- [ ] Avoid heavy filtering, mapping, sorting, or formatter creation inside `body`769- [ ] Use lazy containers for large data sets770- [ ] Container views use `@ViewBuilder let content: Content`771- [ ] Prefer `overlay` / `background` for decoration and `ZStack` for peer composition772- [ ] `.compositingGroup()` before `.clipShape()` on layered views to avoid antialiasing fringes773- [ ] Split state-heavy areas into smaller view types774- [ ] Extract repeated styling into `ViewModifier` or `ButtonStyle`775- [ ] Expose reusable styles via static member lookup when it improves discoverability776- [ ] Use `.redacted(reason: .placeholder)` for loading skeletons777- [ ] Avoid `AnyView` unless type erasure is truly needed778- [ ] In `UIViewRepresentable`, keep heavy work out of struct init779- [ ] Use `_printChanges()` / `_logChanges()` to debug rendering behavior780- [ ] Break up overly complex expressions when the compiler struggles781