Skip to main content
Technology & EngineeringIos Swift289 lines

Swiftui

SwiftUI declarative UI framework fundamentals for building modern iOS interfaces

Quick Summary28 lines
You are an expert in SwiftUI for building iOS applications with Swift.

## Key Points

- **`@State`** — Private, view-local mutable state for simple value types.
- **`@Binding`** — Two-way reference to state owned by a parent view.
- **`@StateObject`** — Creates and owns an `ObservableObject` instance (one-time init).
- **`@ObservedObject`** — Observes an `ObservableObject` passed in from outside.
- **`@EnvironmentObject`** — Reads a shared object injected into the environment.
- **`@Environment`** — Reads system-provided environment values (color scheme, locale, etc.).
- **Keep views small and composable.** Extract subviews and use `ViewModifier` for reusable styling. A view's `body` should be easy to read at a glance.
- **Use the right state wrapper.** `@State` for local values, `@StateObject` to own objects, `@ObservedObject` for injected objects. Misuse causes redundant re-creates or lost state.
- **Prefer `@Observable` on iOS 17+.** It is simpler, more performant, and avoids the `@Published` boilerplate of `ObservableObject`.
- **Minimize work in `body`.** The `body` property can be called many times. Avoid heavy computation; move it to view models or use `.task {}`.
- **Use `LazyVStack` / `LazyHStack` inside `ScrollView`.** Eager stacks instantiate all children upfront; lazy stacks instantiate on demand.
- **Leverage previews.** Provide sample data to `#Preview` macros so you can iterate on UI without running the full app.

## Quick Example

```swift
Text("Hello")
    .padding()           // padding is inside the background
    .background(.blue)   // background covers the padded area
    .padding()           // extra padding outside the background
    .clipShape(RoundedRectangle(cornerRadius: 8))
```
skilldb get ios-swift-skills/SwiftuiFull skill: 289 lines
Paste into your CLAUDE.md or agent config

SwiftUI — iOS/Swift

You are an expert in SwiftUI for building iOS applications with Swift.

Core Philosophy

SwiftUI is built on a single powerful idea: the UI is a function of state. You declare what the screen should look like for any given state, and the framework figures out what changed and updates only the affected parts of the view hierarchy. This declarative model eliminates the imperative boilerplate of "when X happens, update Y" that made UIKit code hard to maintain. The developer's job shifts from managing UI mutations to managing state correctly -- and if the state is right, the UI is right.

Views in SwiftUI are lightweight value types, not heavyweight objects. Creating a Text or a VStack is no more expensive than creating an Int -- the framework evaluates your body property, diffs the result against the previous output, and updates only what changed. This means you should not fear creating many small views. Extracting a subview into its own struct is not a performance concern; it is good architecture. The framework rewards composition.

The property wrapper system (@State, @StateObject, @ObservedObject, @Binding, @Environment) is the control surface for SwiftUI's data flow. Using the wrong wrapper causes subtle bugs: @ObservedObject where @StateObject is needed causes the object to be recreated on parent recomposition; @State for reference types does not trigger re-renders. Understanding which wrapper to use and when is not optional -- it is the foundation of correct SwiftUI code.

Anti-Patterns

  • Performing side effects inside the body property: Mutating state, starting network requests, or logging inside body causes infinite re-render loops or unpredictable behavior. Side effects belong in .task {}, .onAppear, or event handlers like button actions -- never in the body computation itself.

  • Using @ObservedObject when @StateObject is required: If a view creates an ObservableObject and stores it as @ObservedObject, the object is destroyed and recreated every time the parent view re-evaluates. Use @StateObject when the view owns the object's lifecycle, and @ObservedObject only when the object is passed in from a parent.

  • Ignoring modifier order: .padding().background(.blue) produces a blue background around the padded content, while .background(.blue).padding() produces a blue background only behind the content with transparent padding outside. Each modifier wraps the result of the previous one; the order is the meaning.

  • Using eager stacks inside ScrollView: Placing a VStack with hundreds of children inside a ScrollView instantiates all children upfront, even those far off screen. Use LazyVStack or LazyHStack so children are instantiated on demand as they scroll into view.

  • Over-observing large models: If a view only needs one property from a large ObservableObject, it still rebuilds when any @Published property changes. Break models into smaller observable pieces, use the @Observable macro on iOS 17+ for fine-grained tracking, or use Combine publishers for specific properties.

Overview

SwiftUI is Apple's declarative UI framework introduced in iOS 13. It replaces the imperative UIKit approach with a reactive, state-driven model where the UI is a function of its state. Views are lightweight value types (structs) that conform to the View protocol, and SwiftUI efficiently diffs and re-renders only what changes.

Core Concepts

Views and the View Protocol

Every UI element in SwiftUI is a struct conforming to View. The single requirement is a computed body property that returns some View.

struct ProfileCard: View {
    let username: String
    let avatarURL: URL?

    var body: some View {
        HStack(spacing: 12) {
            AsyncImage(url: avatarURL) { image in
                image.resizable().scaledToFill()
            } placeholder: {
                Circle().fill(.gray.opacity(0.3))
            }
            .frame(width: 48, height: 48)
            .clipShape(Circle())

            VStack(alignment: .leading) {
                Text(username)
                    .font(.headline)
                Text("Online")
                    .font(.caption)
                    .foregroundStyle(.green)
            }
        }
        .padding()
        .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12))
    }
}

State Management

SwiftUI provides several property wrappers to manage data flow:

  • @State — Private, view-local mutable state for simple value types.
  • @Binding — Two-way reference to state owned by a parent view.
  • @StateObject — Creates and owns an ObservableObject instance (one-time init).
  • @ObservedObject — Observes an ObservableObject passed in from outside.
  • @EnvironmentObject — Reads a shared object injected into the environment.
  • @Environment — Reads system-provided environment values (color scheme, locale, etc.).
struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack(spacing: 16) {
            Text("Count: \(count)")
                .font(.largeTitle)

            HStack {
                Button("Decrement") { count -= 1 }
                Button("Increment") { count += 1 }
            }
            .buttonStyle(.borderedProminent)
        }
    }
}

Observable Macro (iOS 17+)

The @Observable macro simplifies observation, eliminating the need for @Published and ObservableObject:

@Observable
class SettingsModel {
    var isDarkMode = false
    var fontSize: Double = 14
    var username = ""
}

struct SettingsView: View {
    @State private var settings = SettingsModel()

    var body: some View {
        Form {
            Toggle("Dark Mode", isOn: $settings.isDarkMode)
            Slider(value: $settings.fontSize, in: 10...24) {
                Text("Font Size: \(settings.fontSize, specifier: "%.0f")")
            }
            TextField("Username", text: $settings.username)
        }
    }
}

Layout System

SwiftUI uses a three-step layout process: parent proposes size, child reports size, parent places child.

struct DashboardView: View {
    var body: some View {
        VStack(spacing: 0) {
            headerSection
                .frame(maxWidth: .infinity)
                .background(.blue.gradient)

            LazyVGrid(columns: [
                GridItem(.flexible()),
                GridItem(.flexible())
            ], spacing: 16) {
                ForEach(0..<4) { index in
                    MetricCard(index: index)
                }
            }
            .padding()

            Spacer()
        }
    }

    private var headerSection: some View {
        Text("Dashboard")
            .font(.largeTitle.bold())
            .foregroundStyle(.white)
            .padding()
    }
}

View Modifiers

Modifiers return new views wrapping the original. Order matters because each modifier wraps the result of the previous one.

Text("Hello")
    .padding()           // padding is inside the background
    .background(.blue)   // background covers the padded area
    .padding()           // extra padding outside the background
    .clipShape(RoundedRectangle(cornerRadius: 8))

Custom view modifiers encapsulate reusable styling:

struct CardModifier: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(.background)
            .clipShape(RoundedRectangle(cornerRadius: 12))
            .shadow(color: .black.opacity(0.1), radius: 4, y: 2)
    }
}

extension View {
    func cardStyle() -> some View {
        modifier(CardModifier())
    }
}

Implementation Patterns

Lists and Scrollable Content

struct ContactListView: View {
    @State private var contacts: [Contact] = Contact.samples
    @State private var searchText = ""

    var filteredContacts: [Contact] {
        if searchText.isEmpty { return contacts }
        return contacts.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
    }

    var body: some View {
        List {
            ForEach(filteredContacts) { contact in
                NavigationLink(value: contact) {
                    ContactRow(contact: contact)
                }
            }
            .onDelete { indexSet in
                contacts.remove(atOffsets: indexSet)
            }
        }
        .searchable(text: $searchText, prompt: "Search contacts")
        .listStyle(.insetGrouped)
    }
}

Sheets, Alerts, and Presentations

struct ItemDetailView: View {
    @State private var showDeleteAlert = false
    @State private var showEditSheet = false

    var body: some View {
        VStack {
            Button("Edit") { showEditSheet = true }
            Button("Delete", role: .destructive) { showDeleteAlert = true }
        }
        .sheet(isPresented: $showEditSheet) {
            EditItemView()
                .presentationDetents([.medium, .large])
        }
        .alert("Delete Item?", isPresented: $showDeleteAlert) {
            Button("Cancel", role: .cancel) { }
            Button("Delete", role: .destructive) { deleteItem() }
        } message: {
            Text("This action cannot be undone.")
        }
    }
}

Animations

struct AnimatedToggle: View {
    @State private var isExpanded = false

    var body: some View {
        VStack {
            Button("Toggle") {
                withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
                    isExpanded.toggle()
                }
            }

            RoundedRectangle(cornerRadius: 12)
                .fill(.blue.gradient)
                .frame(width: isExpanded ? 200 : 80, height: isExpanded ? 200 : 80)
                .rotationEffect(.degrees(isExpanded ? 90 : 0))
        }
    }
}

Best Practices

  • Keep views small and composable. Extract subviews and use ViewModifier for reusable styling. A view's body should be easy to read at a glance.
  • Use the right state wrapper. @State for local values, @StateObject to own objects, @ObservedObject for injected objects. Misuse causes redundant re-creates or lost state.
  • Prefer @Observable on iOS 17+. It is simpler, more performant, and avoids the @Published boilerplate of ObservableObject.
  • Minimize work in body. The body property can be called many times. Avoid heavy computation; move it to view models or use .task {}.
  • Use LazyVStack / LazyHStack inside ScrollView. Eager stacks instantiate all children upfront; lazy stacks instantiate on demand.
  • Leverage previews. Provide sample data to #Preview macros so you can iterate on UI without running the full app.

Common Pitfalls

  • Initializing @StateObject with external parameters in init. The object may be recreated unexpectedly. Pass dependencies through a factory or use .task {} to configure after creation.
  • Mutating @State during body evaluation. This triggers an infinite re-render loop. State mutations must happen in event handlers, .onAppear, or .task.
  • Ignoring modifier order. .background before .padding yields a different result than .padding before .background. Always reason about what each modifier wraps.
  • Using @ObservedObject when you mean @StateObject. If the parent re-creates the child view, an @ObservedObject will get a fresh instance, losing state.
  • Over-observing. If a view only needs one property from a large model, the entire view rebuilds when any published property changes. Break models into smaller observable pieces or use the @Observable macro which tracks access automatically.

Install this skill directly: skilldb add ios-swift-skills

Get CLI access →