SwiftUI Introduction

Section 1: List, VStack & HStack & VGrid

List

List is SwiftUI’s equivalent of UITableView. It provides a scrollable, vertically stacked collection of rows grouped into sections. Note that List is inherently single-column (like UITableView) — it does not support multiple items per row. For grid/collection-view layouts, use LazyVGrid instead.

struct FruitListView: View {
    let tropical = ["Mango", "Pineapple", "Papaya"]
    let berries = ["Strawberry", "Blueberry", "Raspberry", "Blackberry"]

    var body: some View {
        List {
            Section(header: Text("Tropical")) {
                ForEach(tropical, id: \.self) { fruit in
                    Text(fruit)
                        // .frame(height: 60)  // specify cell item height
                }
            }
            // .listSectionSpacing(20)  // distance between sections (iOS 17+)

            Section(header: Text("Berries")) {
                ForEach(berries, id: \.self) { fruit in
                    Text(fruit)
                        // .frame(height: 60)
                }
            }
            // .listSectionSpacing(20)
        }
        .listStyle(.plain)
        // .listRowSeparator(.hidden)  // remove the separator line between rows
    }
}

Annotated Layout Mockup:

┌──────────────────────────────────┐
│  List (scrollable)               │
│┌────────────────────────────────┐│
││ Section Header: "Tropical"     ││
│├────────────────────────────────┤│
││ ┌────────────────────────────┐ ││
││ │ Mango                      │ ││ ← row height controlled by .frame(height:)
││ └────────────────────────────┘ ││
││ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─  ││ ← separator (hidden with .listRowSeparator(.hidden))
││ ┌────────────────────────────┐ ││
││ │ Pineapple                  │ ││
││ └────────────────────────────┘ ││
││ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─  ││
││ ┌────────────────────────────┐ ││
││ │ Papaya                     │ ││
││ └────────────────────────────┘ ││
││         ... (N items via       ││
││          ForEach loop)         ││
│└────────────────────────────────┘│
│                                  │
│  ↕ spacing controlled by         │
│    .listSectionSpacing(20)       │
│                                  │
│┌────────────────────────────────┐│
││ Section Header: "Berries"      ││
│├────────────────────────────────┤│
││ ┌────────────────────────────┐ ││
││ │ Strawberry                 │ ││
││ └────────────────────────────┘ ││
││ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─  ││
││ ┌────────────────────────────┐ ││
││ │ Blueberry                  │ ││
││ └────────────────────────────┘ ││
││ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─  ││
││ ┌────────────────────────────┐ ││
││ │ Raspberry                  │ ││
││ └────────────────────────────┘ ││
││ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─  ││
││ ┌────────────────────────────┐ ││
││ │ Blackberry                 │ ││
││ └────────────────────────────┘ ││
││         ... (N items via       ││
││          ForEach loop)         ││
│└────────────────────────────────┘│
└──────────────────────────────────┘

VStack & HStack

VStack arranges views vertically; HStack arranges views horizontally. They can be nested to build complex layouts.

struct ProfileCardView: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("User Profile")
                .font(.title)

            HStack(spacing: 16) {
                Image(systemName: "person.circle.fill")
                    .resizable()
                    .frame(width: 50, height: 50)

                VStack(alignment: .leading, spacing: 4) {
                    Text("Alice")
                        .font(.headline)
                    Text("iOS Developer")
                        .font(.subheadline)
                        .foregroundColor(.gray)
                }

                Spacer()

                Image(systemName: "chevron.right")
            }

            HStack(spacing: 12) {
                Label("42 Posts", systemImage: "doc.text")
                Label("128 Followers", systemImage: "person.2")
            }
            .font(.caption)
        }
        .padding()
    }
}

Annotated Layout Mockup:

┌─── VStack (alignment: .leading, spacing: 12) ───────────────────┐
│                                                                   │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │ "User Profile"  (.title)                                    │ │
│  └─────────────────────────────────────────────────────────────┘ │
│                                                                   │
│  ↕ 12pt spacing                                                   │
│                                                                   │
│  ┌─── HStack (spacing: 16) ─────────────────────────────────────┐│
│  │                                                               ││
│  │  ┌──────┐    ┌─ VStack ──────────┐              ┌───┐        ││
│  │  │      │    │ "Alice"           │              │ > │        ││
│  │  │  👤  │    │  (.headline)      │   Spacer()   │   │        ││
│  │  │      │    │ "iOS Developer"   │   ←─────→    └───┘        ││
│  │  │50x50 │    │  (.subheadline)   │                           ││
│  │  └──────┘    └───────────────────┘                           ││
│  │                                                               ││
│  │  ←─ 16 ─→    ←──── 16 ────→                                  ││
│  └───────────────────────────────────────────────────────────────┘│
│                                                                   │
│  ↕ 12pt spacing                                                   │
│                                                                   │
│  ┌─── HStack (spacing: 12) ────────────────────┐                 │
│  │  📄 "42 Posts"     👥 "128 Followers"        │                 │
│  │  ←──── 12 ────→                              │                 │
│  └──────────────────────────────────────────────┘                 │
│                                                                   │
└───────────────────────────────────────────────────────────────────┘

LazyVGrid

LazyVGrid is SwiftUI’s equivalent of UICollectionView. It supports multiple items per row by defining columns with GridItem. Views are created lazily — only when they scroll into the visible area.

struct FruitGridView: View {
    let fruits = ["Mango", "Pineapple", "Papaya", "Strawberry",
                  "Blueberry", "Raspberry", "Blackberry", "Guava", "Lychee"]

    // Define 3 flexible columns → 3 items per row
    let columns = Array(repeating: GridItem(.flexible(), spacing: 12), count: 3)

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 16) {
                ForEach(fruits, id: \.self) { fruit in
                    Text(fruit)
                        .frame(maxWidth: .infinity, minHeight: 60)
                        .background(Color.blue.opacity(0.1))
                        .cornerRadius(8)
                }
            }
            .padding()
        }
    }
}

Annotated Layout Mockup:

┌─── ScrollView ──────────────────────────────────────────────┐
│                                                              │
│  ┌─── LazyVGrid (3 columns, spacing: 16) ────────────────┐  │
│  │                                                        │  │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐            │  │
│  │  │  Mango   │  │Pineapple │  │  Papaya  │  ← row 1   │  │
│  │  │  60pt h  │  │  60pt h  │  │  60pt h  │            │  │
│  │  └──────────┘  └──────────┘  └──────────┘            │  │
│  │                                                        │  │
│  │  ↕ 16pt vertical spacing                               │  │
│  │                                                        │  │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐            │  │
│  │  │Strawberry│  │Blueberry │  │Raspberry │  ← row 2   │  │
│  │  │  60pt h  │  │  60pt h  │  │  60pt h  │            │  │
│  │  └──────────┘  └──────────┘  └──────────┘            │  │
│  │                                                        │  │
│  │  ↕ 16pt vertical spacing                               │  │
│  │                                                        │  │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐            │  │
│  │  │Blackberry│  │  Guava   │  │  Lychee  │  ← row 3   │  │
│  │  │  60pt h  │  │  60pt h  │  │  60pt h  │            │  │
│  │  └──────────┘  └──────────┘  └──────────┘            │  │
│  │                                                        │  │
│  │  ←─ 12 ─→  ←─ 12 ─→                                   │  │
│  │  (horizontal spacing between columns)                  │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Section 2: View Modifiers — Padding, Foreground, Background, SafeAreaInset, Color & Margin

View modifiers in SwiftUI are applied in order — each modifier wraps the result of the previous one. The order matters!

Padding

.padding() adds space inside the view’s frame, pushing content inward.

struct PaddingExample: View {
    var body: some View {
        Text("Hello, SwiftUI!")
            .padding(.horizontal, 20)   // 20pt left & right
            .padding(.vertical, 12)     // 12pt top & bottom
            .background(Color.yellow)

        // Uniform padding on all sides:
        Text("Uniform Padding")
            .padding(16)                // 16pt on all edges
            .background(Color.mint)
    }
}

Annotated Layout Mockup:

┌─── .background(Color.yellow) ─────────────────────┐
│                                                    │
│  ←── 20pt ──→┌────────────────┐←── 20pt ──→      │
│              │                │               │
│   ↕ 12pt     │ Hello, SwiftUI!│    ↕ 12pt     │
│              │                │               │
│              └────────────────┘               │
│                                                    │
└────────────────────────────────────────────────────┘
  padding adds space BETWEEN content and background

Foreground & Background

.foregroundStyle() sets the content color (text, icons). .background() places a view behind the modified view.

struct ForegroundBackgroundExample: View {
    var body: some View {
        Text("Important Notice")
            .font(.headline)
            .foregroundStyle(.white)               // text color
            .padding(16)
            .background(
                RoundedRectangle(cornerRadius: 10)
                    .fill(Color.red)
            )

        // Layered background with overlay
        Text("Layered")
            .foregroundStyle(.blue)
            .padding()
            .background(Color.blue.opacity(0.1))   // light fill
            .background(                           // outer shadow layer
                RoundedRectangle(cornerRadius: 8)
                    .stroke(Color.blue, lineWidth: 2)
            )
    }
}

Annotated Layout Mockup:

┌─── .background(RoundedRectangle, fill: red) ──────┐
│  ╭─────────────────────────────────────────────╮   │
│  │                                             │   │
│  │  "Important Notice"                         │   │
│  │   foregroundStyle(.white) → white text      │   │
│  │                                             │   │
│  ╰─────────────────────────────────────────────╯   │
│    cornerRadius: 10                                 │
└─────────────────────────────────────────────────────┘

┌─── outer .background (stroke: blue, 2pt) ─────────┐
│  ┌─── inner .background (blue @ 0.1 opacity) ───┐ │
│  │                                               │ │
│  │  "Layered"                                    │ │
│  │   foregroundStyle(.blue) → blue text          │ │
│  │                                               │ │
│  └───────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┘
  backgrounds stack: first applied = closest to content

Color

SwiftUI provides system colors, custom colors, and color modifiers.

struct ColorExample: View {
    var body: some View {
        VStack(spacing: 12) {
            // System colors
            Text("System Red")
                .foregroundStyle(.red)

            // Custom color from hex
            Text("Custom Color")
                .foregroundStyle(Color(red: 0.2, green: 0.5, blue: 0.8))

            // Adaptive colors (light/dark mode)
            Text("Adaptive")
                .foregroundStyle(.primary)        // adapts to light/dark
                .background(Color(.systemBackground))

            // Gradient as foreground
            Text("Gradient Text")
                .font(.largeTitle)
                .foregroundStyle(
                    LinearGradient(
                        colors: [.purple, .pink],
                        startPoint: .leading,
                        endPoint: .trailing
                    )
                )
        }
    }
}

Annotated Layout Mockup:

┌─── VStack (spacing: 12) ─────────────────────────┐
│                                                    │
│  "System Red"          ← .red (fixed color)        │
│                                                    │
│  "Custom Color"        ← rgb(0.2, 0.5, 0.8)       │
│                                                    │
│  "Adaptive"            ← .primary (auto light/dark)│
│  ░░░░░░░░░░░░░░░░░░░  ← .systemBackground         │
│                                                    │
│  "Gradient Text"       ← purple ──────→ pink       │
│                                                    │
└────────────────────────────────────────────────────┘

SafeAreaInset

.safeAreaInset() lets you attach persistent content (like a bottom bar) to the edge of a scrollable view while adjusting the safe area so content scrolls without being obscured.

struct SafeAreaInsetExample: View {
    let items = Array(1...20).map { "Item \($0)" }

    var body: some View {
        List(items, id: \.self) { item in
            Text(item)
        }
        .safeAreaInset(edge: .bottom, spacing: 0) {
            // This bar stays pinned at the bottom
            HStack {
                Text("3 items selected")
                Spacer()
                Button("Delete") { }
                    .foregroundStyle(.red)
            }
            .padding()
            .background(.ultraThinMaterial)
        }
        .safeAreaInset(edge: .top, spacing: 0) {
            // Pinned search bar at top
            TextField("Search...", text: .constant(""))
                .textFieldStyle(.roundedBorder)
                .padding()
                .background(.bar)
        }
    }
}

Annotated Layout Mockup:

┌─── Screen ───────────────────────────────────────┐
│  ┌─── .safeAreaInset(edge: .top) ──────────────┐ │
│  │  ┌──────────────────────────────────────┐   │ │
│  │  │  🔍 Search...                        │   │ │ ← pinned top bar
│  │  └──────────────────────────────────────┘   │ │
│  │  ░░░░░░░░░ .background(.bar) ░░░░░░░░░░░   │ │
│  └─────────────────────────────────────────────┘ │
│                                                   │
│  ┌─── List (scrollable area) ──────────────────┐ │
│  │  Item 1                                     │ │
│  │  Item 2                                     │ │
│  │  Item 3                                     │ │ ← content scrolls
│  │  ...                                        │ │   freely between
│  │  Item 18                                    │ │   top & bottom bars
│  │  Item 19                                    │ │
│  │  Item 20   ← not obscured by bottom bar     │ │
│  └─────────────────────────────────────────────┘ │
│                                                   │
│  ┌─── .safeAreaInset(edge: .bottom) ───────────┐ │
│  │  ░░░░░░ .ultraThinMaterial ░░░░░░░░░░░░░░░  │ │
│  │  "3 items selected"       [Delete]          │ │ ← pinned bottom bar
│  └─────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────┘
  Safe area is adjusted → last list item scrolls
  above the bottom bar (not hidden behind it)

Modifier Order Matters (Padding vs Background vs Margin)

SwiftUI has no explicit .margin() modifier. Instead, you achieve margin-like spacing by applying .padding() after .background() — the outer padding acts as margin.

struct ModifierOrderExample: View {
    var body: some View {
        VStack(spacing: 20) {
            // Inner padding → background → outer padding (margin)
            Text("Padding + Margin")
                .padding(12)                    // inner padding (inside background)
                .background(Color.orange)       // background wraps padded content
                .padding(20)                    // outer padding = acts as MARGIN
                .background(Color.gray.opacity(0.2))  // reveals the "margin" area

            // Demonstrates order dependency
            Text("Order Matters!")
                .background(Color.green)        // tight around text (no padding yet)
                .padding(16)                    // padding OUTSIDE the green background
        }
    }
}

Annotated Layout Mockup:

"Padding + Margin" example:

┌─── .background(gray 0.2) — reveals margin area ────────────┐
│                                                             │
│   ←────── 20pt "margin" ──────→                             │
│                                                             │
│   ┌─── .background(orange) ─────────────────────────┐      │
│   │                                                  │      │
│   │  ←─ 12pt padding ─→                              │      │
│   │  ┌──────────────────────────────────────┐       │      │
│   │  │  "Padding + Margin"                  │       │      │
│   │  └──────────────────────────────────────┘       │      │
│   │  ←─ 12pt padding ─→                              │      │
│   │                                                  │      │
│   └──────────────────────────────────────────────────┘      │
│                                                             │
│   ←────── 20pt "margin" ──────→                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

"Order Matters!" example:

                 ┌─ .background(green) ─┐
    ← 16pt →    │  "Order Matters!"     │    ← 16pt →
                 └──────────────────────┘
    ^^^^^^^^^^^^                          ^^^^^^^^^^^^
    This padding is OUTSIDE the green background,
    so the green box stays tight around the text.

Section 3: Async Work with .task and Pull-to-Refresh with .refreshable

.task

.task attaches an async operation to a view’s lifecycle. The task starts when the view appears and is automatically cancelled when the view disappears. It replaces the pattern of calling async work inside .onAppear with manual cancellation.

struct UserListView: View {
    @State private var users: [String] = []
    @State private var isLoading = true
    @State private var errorMessage: String?

    var body: some View {
        NavigationStack {
            Group {
                if isLoading {
                    ProgressView("Loading...")
                } else if let error = errorMessage {
                    Text(error)
                        .foregroundStyle(.red)
                } else {
                    List(users, id: \.self) { user in
                        Text(user)
                    }
                }
            }
            .navigationTitle("Users")
        }
        // .task starts when view appears, cancels when view disappears
        .task {
            await fetchUsers()
        }
    }

    func fetchUsers() async {
        do {
            // Simulating network request
            let url = URL(string: "https://api.example.com/users")!
            let (data, _) = try await URLSession.shared.data(from: url)
            let decoded = try JSONDecoder().decode([String].self, from: data)
            users = decoded
        } catch {
            errorMessage = error.localizedDescription
        }
        isLoading = false
    }
}

Key behaviors of .task:

Behavior Description
Auto-start Runs when the view appears
Auto-cancel Cancelled when the view is removed from hierarchy
Async context Body is async — you can await directly
Main actor Runs on @MainActor by default (safe to update @State)

.task(id:) — Re-run on Value Change

.task(id:) re-triggers the async work whenever the id value changes. The previous task is cancelled before the new one starts.

struct UserDetailView: View {
    @State private var selectedUserID: Int = 1
    @State private var userDetail: String = ""

    var body: some View {
        VStack(spacing: 20) {
            Picker("User", selection: $selectedUserID) {
                Text("User 1").tag(1)
                Text("User 2").tag(2)
                Text("User 3").tag(3)
            }
            .pickerStyle(.segmented)

            Text(userDetail)
                .padding()
        }
        // Re-fetches every time selectedUserID changes
        // Previous in-flight request is automatically cancelled
        .task(id: selectedUserID) {
            userDetail = "Loading..."
            await fetchDetail(for: selectedUserID)
        }
    }

    func fetchDetail(for id: Int) async {
        // Simulate network delay
        try? await Task.sleep(for: .seconds(1))
        userDetail = "Detail for user \(id)"
    }
}

Lifecycle diagram:

Timeline:
─────────────────────────────────────────────────────────────────

View appears          selectedUserID = 1 → 2          View disappears
     │                        │                              │
     ▼                        ▼                              ▼
┌─────────┐              ┌─────────┐                    ┌────────┐
│ .task   │  cancelled → │ .task   │    cancelled →     │ clean  │
│ id: 1   │──────────────│ id: 2   │────────────────────│  up    │
│ starts  │              │ starts  │                    │        │
└─────────┘              └─────────┘                    └────────┘

• Old task cancelled BEFORE new task starts
• View disappearance cancels any running task

.refreshable (Pull-to-Refresh)

.refreshable adds native pull-to-refresh to scrollable views. The refresh indicator automatically shows while the async closure is running and hides when it completes.

struct RefreshableListView: View {
    @State private var posts: [String] = ["Post 1", "Post 2", "Post 3"]
    @State private var lastUpdated = Date()

    var body: some View {
        NavigationStack {
            List {
                Section {
                    ForEach(posts, id: \.self) { post in
                        Text(post)
                    }
                } footer: {
                    Text("Last updated: \(lastUpdated.formatted(date: .omitted, time: .standard))")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }
            }
            .navigationTitle("Feed")
            // Pull-to-refresh
            .refreshable {
                await refreshPosts()
            }
        }
    }

    func refreshPosts() async {
        // Simulate network fetch
        try? await Task.sleep(for: .seconds(2))

        // Update data
        let newPost = "Post \(posts.count + 1)"
        posts.append(newPost)
        lastUpdated = Date()
    }
}

Interaction flow:

┌─── NavigationStack ────────────────────────────────┐
│  ┌─── "Feed" ──────────────────────────────────┐   │
│  │                                              │   │
│  │   ┌─ User pulls down ─────────────────────┐ │   │
│  │   │         ↓  ↓  ↓                       │ │   │
│  │   │    ┌──────────────┐                    │ │   │
│  │   │    │  ◠ Spinner   │ ← shows while      │ │   │
│  │   │    └──────────────┘   awaiting          │ │   │
│  │   └────────────────────────────────────────┘ │   │
│  │                                              │   │
│  │  ┌────────────────────────────────────────┐  │   │
│  │  │  Post 1                                │  │   │
│  │  │  Post 2                                │  │   │
│  │  │  Post 3                                │  │   │
│  │  │  Post 4  ← newly appended after refresh│  │   │
│  │  ├────────────────────────────────────────┤  │   │
│  │  │  Last updated: 10:30:45 AM             │  │   │
│  │  └────────────────────────────────────────┘  │   │
│  └──────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘

Combining .task and .refreshable

A common pattern: use .task for the initial load and .refreshable for user-triggered refresh.

struct CombinedExample: View {
    @State private var articles: [String] = []
    @State private var isLoading = true

    var body: some View {
        NavigationStack {
            Group {
                if isLoading {
                    ProgressView()
                } else {
                    List(articles, id: \.self) { article in
                        Text(article)
                    }
                    .refreshable {
                        // User-triggered refresh (pull down)
                        await loadArticles()
                    }
                }
            }
            .navigationTitle("Articles")
        }
        .task {
            // Initial load when view appears
            await loadArticles()
            isLoading = false
        }
    }

    func loadArticles() async {
        try? await Task.sleep(for: .seconds(1))
        articles = (1...10).map { "Article \($0) - \(Date().formatted(date: .omitted, time: .standard))" }
    }
}

Lifecycle summary:

View Appears
     │
     ▼
┌──────────────────┐
│  .task fires     │ → initial data load
│  isLoading = true│ → shows ProgressView
└────────┬─────────┘
         │ await completes
         ▼
┌──────────────────┐
│  List displayed  │ → .refreshable now available
│  isLoading = false│
└────────┬─────────┘
         │ user pulls down
         ▼
┌──────────────────┐
│  .refreshable    │ → spinner shown automatically
│  await fires     │ → data refreshed
└──────────────────┘ → spinner hides when done

Section 4: NavigationPath — Programmatic Navigation

NavigationPath is a type-erased collection that drives programmatic navigation in a NavigationStack. You can push, pop, and reset the navigation stack by mutating the path.

Basic NavigationPath Usage

struct AppRootView: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            List {
                Section("Fruits") {
                    Button("Go to Mango") {
                        path.append("Mango")       // push by appending
                    }
                    Button("Go to Pineapple") {
                        path.append("Pineapple")
                    }
                }

                Section("Actions") {
                    Button("Deep link: Mango → Detail #42") {
                        // Push multiple screens at once
                        path.append("Mango")
                        path.append(42)            // pushes a different type
                    }
                }
            }
            .navigationTitle("Home")
            // Define destinations for each data type
            .navigationDestination(for: String.self) { fruit in
                FruitDetailView(fruit: fruit, path: $path)
            }
            .navigationDestination(for: Int.self) { id in
                ItemDetailView(itemID: id)
            }
        }
    }
}

struct FruitDetailView: View {
    let fruit: String
    @Binding var path: NavigationPath

    var body: some View {
        VStack(spacing: 20) {
            Text(fruit)
                .font(.largeTitle)

            Button("Go deeper → Item #7") {
                path.append(7)                     // push another screen
            }

            Button("Back to root") {
                path = NavigationPath()            // reset = pop to root
            }
        }
        .navigationTitle(fruit)
    }
}

struct ItemDetailView: View {
    let itemID: Int

    var body: some View {
        Text("Item Detail #\(itemID)")
            .font(.title)
            .navigationTitle("Item \(itemID)")
    }
}

Navigation stack diagram:

path = []                    path = ["Mango"]           path = ["Mango", 42]
┌────────────┐              ┌────────────┐              ┌────────────┐
│            │   append     │            │   append     │            │
│   Home     │──"Mango"──→  │   Fruit    │────42────→   │   Item     │
│   (List)   │              │   Detail   │              │   Detail   │
│            │              │  "Mango"   │              │   #42      │
└────────────┘              └────────────┘              └────────────┘

Pop to root: path = NavigationPath()  →  returns to Home instantly
Pop one:     path.removeLast()        →  goes back one screen

Pop Control

struct PopExamplesView: View {
    @Binding var path: NavigationPath

    var body: some View {
        VStack(spacing: 16) {
            Button("Pop one screen") {
                path.removeLast()              // go back 1
            }

            Button("Pop 2 screens") {
                path.removeLast(2)             // go back 2
            }

            Button("Pop to root") {
                path = NavigationPath()        // clear entire stack
            }
        }
    }
}

Pop behavior:

Before:  path = ["Mango", 42, 99]

         ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐
         │ Home │→ │Mango │→ │ #42  │→ │ #99  │  (current)
         └──────┘  └──────┘  └──────┘  └──────┘

removeLast():     path = ["Mango", 42]       → back to #42
removeLast(2):    path = ["Mango"]           → back to Mango
path = .init():   path = []                  → back to Home

A clean pattern for apps with many screen types:

enum Route: Hashable {
    case profile(userID: String)
    case settings
    case postDetail(postID: Int)
}

struct MainAppView: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            HomeTabView(path: $path)
                .navigationDestination(for: Route.self) { route in
                    switch route {
                    case .profile(let userID):
                        ProfileView(userID: userID)
                    case .settings:
                        SettingsView()
                    case .postDetail(let postID):
                        PostDetailView(postID: postID)
                    }
                }
        }
    }
}

struct HomeTabView: View {
    @Binding var path: NavigationPath

    var body: some View {
        List {
            Button("My Profile") {
                path.append(Route.profile(userID: "user_123"))
            }
            Button("Settings") {
                path.append(Route.settings)
            }
            Button("View Post #5") {
                path.append(Route.postDetail(postID: 5))
            }
        }
        .navigationTitle("Home")
    }
}

Route-based navigation flow:

┌─────────────────────────────────────────────────────────────────┐
│  NavigationStack(path: $path)                                   │
│                                                                  │
│  ┌─── .navigationDestination(for: Route.self) ───────────────┐  │
│  │                                                            │  │
│  │   Route.profile("user_123")  →  ProfileView               │  │
│  │   Route.settings             →  SettingsView               │  │
│  │   Route.postDetail(5)        →  PostDetailView             │  │
│  │                                                            │  │
│  └────────────────────────────────────────────────────────────┘  │
│                                                                  │
│  path.append(Route.xxx)  → push                                  │
│  path.removeLast()       → pop                                   │
│  path = NavigationPath() → pop to root                           │
└─────────────────────────────────────────────────────────────────┘

Section 5: Animation & Transition

Animation

Animations in SwiftUI automatically interpolate between state changes. You can apply them implicitly (.animation() modifier) or explicitly (withAnimation {}).

Implicit Animation

Attaches an animation to a specific view — animates whenever a tracked value changes.

struct ImplicitAnimationView: View {
    @State private var scale: CGFloat = 1.0
    @State private var rotation: Double = 0

    var body: some View {
        VStack(spacing: 40) {
            Image(systemName: "star.fill")
                .font(.system(size: 60))
                .foregroundStyle(.yellow)
                .scaleEffect(scale)
                .rotationEffect(.degrees(rotation))
                // Animates ANY change to this view's properties
                .animation(.easeInOut(duration: 0.5), value: scale)
                .animation(.spring(response: 0.4, dampingFraction: 0.6), value: rotation)

            HStack(spacing: 20) {
                Button("Scale") {
                    scale = scale == 1.0 ? 1.5 : 1.0
                }
                Button("Rotate") {
                    rotation += 90
                }
            }
        }
    }
}

Animation timeline:

Button tap: scale = 1.0 → 1.5

Frame 0        Frame 5        Frame 10       Frame 15
┌──────┐      ┌────────┐     ┌──────────┐    ┌────────────┐
│  ★   │  →   │   ★    │  →  │    ★     │ →  │     ★      │
│ 1.0x │      │  1.15x │     │   1.35x  │    │    1.5x    │
└──────┘      └────────┘     └──────────┘    └────────────┘

.easeInOut: starts slow, speeds up, slows down at end
.spring:    overshoots slightly, then settles

Explicit Animation (withAnimation)

Wraps a state change — everything that depends on that state animates together.

struct ExplicitAnimationView: View {
    @State private var isExpanded = false
    @State private var opacity: Double = 1.0

    var body: some View {
        VStack(spacing: 20) {
            RoundedRectangle(cornerRadius: isExpanded ? 20 : 50)
                .fill(.blue)
                .frame(
                    width: isExpanded ? 300 : 100,
                    height: isExpanded ? 200 : 100
                )
                .opacity(opacity)

            Button("Toggle") {
                // Everything that reads `isExpanded` animates
                withAnimation(.spring(response: 0.6, dampingFraction: 0.7)) {
                    isExpanded.toggle()
                }
            }

            Button("Fade") {
                withAnimation(.easeOut(duration: 0.3)) {
                    opacity = opacity == 1.0 ? 0.3 : 1.0
                }
            }
        }
    }
}

State change visualization:

isExpanded = false                    isExpanded = true
┌──────────────────┐                 ┌──────────────────────────────────┐
│                  │                 │                                  │
│    ┌──────┐     │   withAnimation │    ┌────────────────────────┐   │
│    │  ●   │     │  ───────────→   │    │                        │   │
│    │100x100│    │   .spring()     │    │       300 x 200        │   │
│    │ r:50 │     │                 │    │       r: 20            │   │
│    └──────┘     │                 │    └────────────────────────┘   │
│                  │                 │                                  │
└──────────────────┘                 └──────────────────────────────────┘
  circle (r=50)                        rounded rect (r=20)

Animation Types

struct AnimationTypesView: View {
    @State private var move = false

    var body: some View {
        VStack(spacing: 16) {
            // Linear — constant speed
            Circle().fill(.red).frame(width: 30)
                .offset(x: move ? 100 : -100)
                .animation(.linear(duration: 1), value: move)

            // Ease In Out — smooth start and end
            Circle().fill(.orange).frame(width: 30)
                .offset(x: move ? 100 : -100)
                .animation(.easeInOut(duration: 1), value: move)

            // Spring — bouncy, natural feel
            Circle().fill(.green).frame(width: 30)
                .offset(x: move ? 100 : -100)
                .animation(.spring(response: 0.5, dampingFraction: 0.3), value: move)

            // Interpolating spring — no bounce
            Circle().fill(.blue).frame(width: 30)
                .offset(x: move ? 100 : -100)
                .animation(.interpolatingSpring(stiffness: 50, damping: 10), value: move)

            Button("Move") { move.toggle() }
        }
    }
}

Comparison of animation curves:

Position over time:

         ●━━━━━━━━━━━━━━━━━━●  linear (constant speed)
        ╱                      ╲
       ╱                        ╲
Start ●───╮                  ╭───● End   easeInOut (S-curve)
           ╰────────────────╯

Start ●───────────────────────●──●──● End   spring (overshoots, settles)
                                ↑
                            overshoot

         .linear         constant velocity
         .easeInOut      accelerates then decelerates
         .spring         physics-based, can bounce
         .interpolatingSpring   physics-based, no overshoot

Transition

Transitions define how a view appears and disappears when inserted into or removed from the view hierarchy. They are paired with if/else or conditional rendering and require an animation context.

Built-in Transitions

struct TransitionExamplesView: View {
    @State private var showCard = false

    var body: some View {
        VStack(spacing: 30) {
            Button(showCard ? "Hide" : "Show") {
                withAnimation(.easeInOut(duration: 0.4)) {
                    showCard.toggle()
                }
            }

            if showCard {
                // Slide in from leading edge, slide out to trailing
                Text("Slide")
                    .padding()
                    .background(Color.blue)
                    .foregroundStyle(.white)
                    .transition(.slide)
            }

            if showCard {
                // Fade in/out
                Text("Opacity")
                    .padding()
                    .background(Color.green)
                    .foregroundStyle(.white)
                    .transition(.opacity)
            }

            if showCard {
                // Scale up from center
                Text("Scale")
                    .padding()
                    .background(Color.purple)
                    .foregroundStyle(.white)
                    .transition(.scale)
            }

            if showCard {
                // Move in from a specific edge
                Text("Move")
                    .padding()
                    .background(Color.orange)
                    .foregroundStyle(.white)
                    .transition(.move(edge: .bottom))
            }
        }
    }
}

Transition behavior:

showCard = false → true (insertion)       showCard = true → false (removal)

.slide:
  ←────────│ Slide │                         │ Slide │────────→
  enters from leading                        exits to trailing

.opacity:
  ░░░░░░░░ → █████████                      █████████ → ░░░░░░░░
  alpha 0 → 1                               alpha 1 → 0

.scale:
       ·  →  ■  →  ████                     ████  →  ■  →  ·
  scale 0 → 1                               scale 1 → 0

.move(edge: .bottom):
                    ┌────┐                   ┌────┐
                    │Move│                    │Move│
       ↑            └────┘                   └────┘            ↓
  enters from below                          exits downward

Asymmetric Transitions

Different animation for insertion vs removal:

struct AsymmetricTransitionView: View {
    @State private var showNotification = false

    var body: some View {
        VStack {
            Button("Toggle Notification") {
                withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
                    showNotification.toggle()
                }
            }

            Spacer()

            if showNotification {
                HStack {
                    Image(systemName: "bell.fill")
                    Text("New message received!")
                }
                .padding()
                .background(Color.blue)
                .foregroundStyle(.white)
                .cornerRadius(12)
                .transition(.asymmetric(
                    insertion: .move(edge: .top).combined(with: .opacity),
                    removal: .scale.combined(with: .opacity)
                ))
            }

            Spacer()
        }
    }
}

Asymmetric flow:

Insertion (appears):                    Removal (disappears):
                                        
  ┌───────────────────┐                 ┌───────────────────┐
  │ 🔔 New message!   │  ← slides       │ 🔔 New message!   │
  └───────────────────┘    down from     └───────────────────┘
           ↓               top +                  ↓
  ┌───────────────────┐    fades in      ┌───────────────────┐
  │ 🔔 New message!   │                 │   🔔 New message!  │ ← shrinks
  └───────────────────┘                 └───────────────────┘   + fades
           ↓                                      ↓
       (settled)                                  ·  (gone)

.combined(with:) merges multiple transitions together

Custom Transitions with .modifier

struct RotateAndFadeModifier: ViewModifier {
    let isActive: Bool

    func body(content: Content) -> some View {
        content
            .rotationEffect(.degrees(isActive ? 90 : 0))
            .opacity(isActive ? 0 : 1)
            .scaleEffect(isActive ? 0.5 : 1)
    }
}

extension AnyTransition {
    static var rotateAndFade: AnyTransition {
        .modifier(
            active: RotateAndFadeModifier(isActive: true),
            identity: RotateAndFadeModifier(isActive: false)
        )
    }
}

struct CustomTransitionView: View {
    @State private var showElement = false

    var body: some View {
        VStack(spacing: 30) {
            Button("Toggle") {
                withAnimation(.easeInOut(duration: 0.6)) {
                    showElement.toggle()
                }
            }

            if showElement {
                RoundedRectangle(cornerRadius: 16)
                    .fill(.indigo)
                    .frame(width: 150, height: 150)
                    .transition(.rotateAndFade)   // custom transition
            }
        }
    }
}

Custom transition states:

identity (visible):              active (hidden):

┌──────────────────┐              ·    ╱╲
│                  │                  ╱    ╲    ← rotated 90°
│    ████████      │              ╱  50%    ╲   ← scaled 0.5x
│    ████████      │              ╲  opacity ╱   ← faded
│                  │                  ╲    ╱
└──────────────────┘              ·    ╲╱

SwiftUI interpolates between identity ↔ active states

Section 6: State Management — How SwiftUI Works Behind the Scenes

The Reactive View Hierarchy

SwiftUI is a declarative, reactive framework. You describe what the UI should look like for a given state, and SwiftUI figures out how to update the screen efficiently. Under the hood, it builds a dependency graph that connects dynamic properties (data) to view bodies (UI).

Views Are Value Types, Not Persistent Objects

Unlike UIKit where UIView instances persist in memory and you mutate them imperatively, SwiftUI View structs are lightweight descriptions — they’re created, diffed, and discarded every render cycle. The persistent state lives in the framework’s internal storage (the attribute graph), not in your view structs.

struct CounterView: View {
    @State private var count = 0       // ← stored in SwiftUI's internal graph, NOT in struct

    var body: some View {              // ← called when dependency changes
        VStack {
            Text("Count: \(count)")    // ← depends on `count`
            Button("+1") { count += 1 }
        }
    }
}

What actually happens in memory:

Your code (value types):              SwiftUI's internal storage (persistent):
┌─────────────────────────┐           ┌──────────────────────────────────┐
│  struct CounterView      │           │  Attribute Graph                 │
│  (recreated each render) │           │  ┌────────────────────────────┐ │
│                          │           │  │  Node: count = 0           │ │
│  @State var count ───────────────→   │  │  Subscribers: [body]       │ │
│                          │           │  └────────────────────────────┘ │
│  var body: some View     │           │                                  │
│    → VStack              │           │  Rendered View Tree (persistent) │
│      → Text              │           │  ┌────────────────────────────┐ │
│      → Button            │           │  │  VStack                    │ │
│                          │           │  │   ├── Text("Count: 0")     │ │
└─────────────────────────┘           │  │   └── Button("+1")         │ │
                                       │  └────────────────────────────┘ │
                                       └──────────────────────────────────┘

The Dependency Graph

SwiftUI maintains an attribute graph — a directed graph that connects property dependencies (sources of truth) to view body dependents (consumers of that data).

How the Dependency Graph is Built

When SwiftUI first evaluates a view’s body, it tracks which dynamic properties are read. This creates edges in the dependency graph:

struct ProfileView: View {
    @State private var name = "Alice"         // dependency A
    @State private var age = 30               // dependency B
    @State private var showBadge = false      // dependency C

    var body: some View {
        VStack {
            Text(name)                         // reads A
            Text("Age: \(age)")                // reads B

            if showBadge {                     // reads C
                Image(systemName: "star")      // conditionally exists
            }
        }
    }
}

Dependency graph constructed:

┌─── Property Dependencies (Sources of Truth) ───────────────────────┐
│                                                                      │
│  ┌─────────┐       ┌─────────┐       ┌──────────────┐              │
│  │  @State │       │  @State │       │    @State    │              │
│  │  name   │       │   age   │       │  showBadge   │              │
│  │ "Alice" │       │   30    │       │    false     │              │
│  └────┬────┘       └────┬────┘       └──────┬───────┘              │
│       │                  │                   │                       │
└───────│──────────────────│───────────────────│───────────────────────┘
        │                  │                   │
        │   subscribes     │   subscribes      │   subscribes
        └──────────────────┼───────────────────┘
                           │
                           ▼
┌─── View Body (the single subscriber) ───────────────────────────────┐
│                                                                      │
│  ProfileView.body                                                    │
│  ┌────────────────────────────────────────────────────────────────┐ │
│  │  VStack {                                                      │ │
│  │      Text(name)              ← reads A                         │ │
│  │      Text("Age: \(age)")     ← reads B                         │ │
│  │      if showBadge { ... }    ← reads C                         │ │
│  │  }                                                             │ │
│  └────────────────────────────────────────────────────────────────┘ │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘

The `body` computed property is the subscriber — NOT individual subviews.
Since `body` reads name, age, AND showBadge during evaluation,
changing ANY of them invalidates the ENTIRE body for re-evaluation.

  name changes      → body re-evaluated (entire VStack re-described)
  age changes       → body re-evaluated (entire VStack re-described)
  showBadge changes → body re-evaluated (entire VStack re-described)

After re-evaluation, the DIFF mechanism determines which render
nodes actually changed and need updating on screen.

Registration Happens at Read Time

The dependency tracking is automatic and implicit. SwiftUI doesn’t scan your code statically — it observes which properties are actually accessed during body evaluation. The subscriber is the view’s body as a whole:

body evaluation starts
    │
    ├── reads `name`     → registers ProfileView.body as subscriber of `name`
    ├── reads `age`      → registers ProfileView.body as subscriber of `age`
    ├── reads `showBadge`→ registers ProfileView.body as subscriber of `showBadge`
    │     └── showBadge == false → does NOT evaluate inner Image
    │                              (Image never produced in this pass)
    │
body evaluation ends

Result: ProfileView.body is subscribed to ALL THREE properties.
        Any single change → entire body re-evaluated → then diffed.

Property Wrappers: Access-Based vs Declaration-Based Dependency Tracking

Not all property wrappers track dependencies the same way. The critical distinction:

  • Declaration-based: The view subscribes to the property merely by declaring it. Even if body never reads the property, any change will still trigger re-evaluation.
  • Access-based: The view subscribes only when body actually accesses a specific property on the observed object. Unread properties don’t create dependencies.

Overview

Property Wrapper Tracking  
@State Access-based value-level diffing before sending invalidation signal
@Binding Declaration-based value-level diffing before sending invalidation signal
@Bindable Access-based  
@Observable (macro) Access-based  
@Environment (value-type key) Declaration-based  
@Environment (@Observable object) Access-based  
@StateObject Declaration-based  
@ObservedObject Declaration-based  
@EnvironmentObject Declaration-based  

Detailed Comparison

Property Wrapper Tracking Method Invalidation Trigger
@State Access-based Dependency registered when body reads the value. Since @State holds a single value, reading it = subscribing to it. The tracking mechanism is access-based in SwiftUI’s attribute graph
@Binding Declaration-based The parent pushes a new value down the hierarchy. When the underlying source changes, the child view always re-evaluates — regardless of whether body reads the binding
@Bindable Access-based Only the specific properties read in body are tracked. Unread properties do NOT trigger re-evaluation
@Observable (macro) Access-based When a view reads an @Observable object’s properties, only those specific properties create subscriptions
@Environment (value-type key) Declaration-based Dependency registered when the view declares the environment key. The view subscribes to that key regardless of whether body reads it — merely declaring @Environment(\.key) creates the subscription
@Environment (@Observable object) Access-based When the environment value is an @Observable object, the Observation framework takes over. Merely declaring the property does NOT subscribe — only properties actually read during body evaluation create dependencies
@StateObject Declaration-based Any @Published property change on the object invalidates the view, regardless of which property body reads. Uses Combine’s objectWillChange — a single coarse signal
@ObservedObject Declaration-based Same as @StateObject — any objectWillChange signal invalidates the view
@EnvironmentObject Declaration-based Any @Published change on the injected object invalidates the view. Same Combine-based coarse signal

@State — Access-Based

@State creates a source of truth owned by the view. The dependency is registered when body reads the value — this is access-based tracking in SwiftUI’s attribute graph. Since @State wraps a single value, reading it subscribes you to that one value.

┌─── Access-based ──────────────────────────────────────────┐
│                                                            │
│  @State var count = 0                                      │
│                                                            │
│  var body: some View {                                     │
│      Text("\(count)")  ← reading `count` registers the     │
│  }                       dependency in the attribute graph  │
│                                                            │
│  The subscription is created at the moment of ACCESS       │
│  (when body reads the wrappedValue), not at declaration.   │
│                                                            │
│  In practice, you almost always read @State in body,       │
│  so it always triggers re-evaluation on change.            │
│  But the mechanism itself is access-based.                 │
│                                                            │
└────────────────────────────────────────────────────────────┘

@StateObject / @ObservedObject — Declaration-Based

Both observe an ObservableObject (using Combine’s objectWillChange publisher). The subscription is coarse-grained — when any @Published property on the object emits, the view is invalidated, even if body only reads one property.

┌─── Declaration-based (coarse-grained) ────────────────────┐
│                                                            │
│  class UserModel: ObservableObject {                       │
│      @Published var name = "Alice"    // property X        │
│      @Published var email = "a@b.com" // property Y        │
│      @Published var avatar = UIImage()// property Z        │
│  }                                                         │
│                                                            │
│  struct ProfileView: View {                                │
│      @StateObject var user = UserModel()                   │
│                                                            │
│      var body: some View {                                 │
│          Text(user.name)  // only reads X                  │
│      }                                                     │
│  }                                                         │
│                                                            │
│  user.email = "new@b.com"  → body RE-EVALUATED!           │
│  user.avatar = newImage    → body RE-EVALUATED!           │
│                                                            │
│  Even though body only reads `name`, ANY @Published        │
│  change fires `objectWillChange` → invalidates view.       │
│                                                            │
└────────────────────────────────────────────────────────────┘

@StateObject vs @ObservedObject:

  @StateObject @ObservedObject
Ownership View creates & owns the object View receives it (doesn’t own)
Lifecycle Survives view re-creation (persisted by SwiftUI) May be destroyed if parent re-creates the view
Use when This view is the source of truth Parent passes the object down

@EnvironmentObject — Declaration-Based

Same Combine-based objectWillChange mechanism as @StateObject/@ObservedObject, but injected via the environment. Still declaration-based — any @Published change invalidates all views that declared @EnvironmentObject of that type.

┌─── Declaration-based (injected via environment) ──────────┐
│                                                            │
│  class ThemeManager: ObservableObject {                    │
│      @Published var primaryColor = Color.blue              │
│      @Published var fontSize: CGFloat = 16                 │
│      @Published var isDark = false                         │
│  }                                                         │
│                                                            │
│  struct TitleView: View {                                  │
│      @EnvironmentObject var theme: ThemeManager            │
│                                                            │
│      var body: some View {                                 │
│          Text("Hello")                                     │
│              .foregroundStyle(theme.primaryColor)           │
│      }                                                     │
│  }                                                         │
│                                                            │
│  theme.isDark = true  → TitleView.body RE-EVALUATED!      │
│                                                            │
│  body only reads `primaryColor`, but `isDark` change       │
│  still fires objectWillChange → invalidates TitleView.     │
│                                                            │
└────────────────────────────────────────────────────────────┘

@Environment — Two Scenarios

@Environment behaves differently depending on what type of value it holds:

Scenario 1: Value-Type Environment Keys — Declaration-Based

For built-in keys (e.g., \.colorScheme, \.locale) or custom EnvironmentKey with value types, the dependency is registered when the view declares the key. The view subscribes regardless of whether body reads it.

┌─── Declaration-based (value-type key) ────────────────────┐
│                                                            │
│  struct AdaptiveView: View {                               │
│      @Environment(\.colorScheme) var colorScheme           │
│                                                            │
│      var body: some View {                                 │
│          Text("Hello")                                     │
│              .foregroundStyle(                              │
│                  colorScheme == .dark ? .white : .black     │
│              )                                             │
│      }                                                     │
│  }                                                         │
│                                                            │
│  System switches light↔dark → body re-evaluated            │
│  (subscribed because @Environment(\.colorScheme) was       │
│   declared — even if body never read `colorScheme`)        │
│                                                            │
│  Note: @Environment(\.locale) on a DIFFERENT view          │
│  would NOT invalidate AdaptiveView — each view only        │
│  subscribes to the keys it declares.                       │
│                                                            │
└────────────────────────────────────────────────────────────┘

Scenario 2: @Observable Objects via Environment — Access-Based

When the environment value is an @Observable object (iOS 17+), the Observation framework takes over dependency tracking. Merely declaring the property does NOT subscribe the view to the object’s changes. The view is only invalidated if a specific property on the object was read during body evaluation and that property is mutated.

┌─── Access-based (@Observable in environment) ─────────────┐
│                                                            │
│  @Observable                                               │
│  class AppSettings {                                       │
│      var fontSize: CGFloat = 14                            │
│      var theme: String = "default"                         │
│      var debugMode: Bool = false                           │
│  }                                                         │
│                                                            │
│  struct ContentView: View {                                │
│      @Environment(AppSettings.self) var settings           │
│                                                            │
│      var body: some View {                                 │
│          Text("Hello")                                     │
│              .font(.system(size: settings.fontSize))       │
│              // only reads `fontSize`                      │
│      }                                                     │
│  }                                                         │
│                                                            │
│  settings.fontSize = 18   → body RE-EVALUATED ✓            │
│  settings.theme = "dark"  → body NOT re-evaluated ✗        │
│  settings.debugMode = true→ body NOT re-evaluated ✗        │
│                                                            │
│  Observation framework tracks per-property access.         │
│  Only `fontSize` was read → only `fontSize` mutations      │
│  trigger invalidation.                                     │
│                                                            │
└────────────────────────────────────────────────────────────┘

@Binding — Declaration-Based

@Binding is a two-way reference to a source of truth owned elsewhere. The parent view pushes a new value down the hierarchy. When the underlying source changes, the child view always re-evaluates — regardless of whether body actually reads the binding’s value.

┌─── Declaration-based (parent pushes value) ───────────────┐
│                                                            │
│  struct ToggleRow: View {                                  │
│      @Binding var isOn: Bool   // connected to parent's    │
│                                // @State                    │
│      var body: some View {                                 │
│          Toggle("Enable", isOn: $isOn)                     │
│      }                                                     │
│  }                                                         │
│                                                            │
│  Parent changes the @State that this binding points to     │
│  → ToggleRow.body is ALWAYS re-evaluated                   │
│    (even if body never read `isOn` directly)               │
│                                                            │
│  The binding is a live connection to the parent's source   │
│  of truth. Declaring it subscribes the child to changes    │
│  from the parent — no body access required.                │
│                                                            │
└────────────────────────────────────────────────────────────┘

@Bindable — Access-Based (Observation framework, iOS 17+)

@Bindable is used with @Observable classes (the new Observation framework). Unlike Combine-based wrappers, tracking is access-based — SwiftUI tracks exactly which properties are read during body and only subscribes to those.

┌─── Access-based (fine-grained, per-property) ─────────────┐
│                                                            │
│  @Observable                                               │
│  class UserModel {                                         │
│      var name = "Alice"       // property X                │
│      var email = "a@b.com"    // property Y                │
│      var avatar = UIImage()   // property Z                │
│  }                                                         │
│                                                            │
│  struct ProfileView: View {                                │
│      @Bindable var user: UserModel                         │
│                                                            │
│      var body: some View {                                 │
│          Text(user.name)             // READS X directly   │
│          TextField("Email", text: $user.email)             │
│          // $user.email is a Binding — does NOT read Y     │
│      }                                                     │
│  }                                                         │
│                                                            │
│  user.name = "Bob"     → body RE-EVALUATED ✓ (read in     │
│                           body via Text(user.name))        │
│  user.email = "b@c.com"→ body NOT re-evaluated ✗           │
│                           ($user.email is a binding, not   │
│                            a read — TextField subscribes   │
│                            internally in its own body)     │
│  user.avatar = img     → body NOT re-evaluated ✗           │
│                                                            │
│  Only `name` was accessed during body evaluation,          │
│  so only `name` changes trigger ProfileView invalidation.  │
│                                                            │
└────────────────────────────────────────────────────────────┘

@Observable (without wrapper) — Access-Based

When you pass an @Observable object as a plain property (no wrapper needed in many cases), SwiftUI still does access-based tracking automatically:

┌─── Access-based (automatic, no wrapper needed) ───────────┐
│                                                            │
│  @Observable                                               │
│  class Counter {                                           │
│      var count = 0                                         │
│      var label = "Items"                                   │
│  }                                                         │
│                                                            │
│  struct CounterView: View {                                │
│      var counter: Counter  // no property wrapper!          │
│                                                            │
│      var body: some View {                                 │
│          Text("\(counter.count)")  // only reads `count`   │
│      }                                                     │
│  }                                                         │
│                                                            │
│  counter.count = 5    → body RE-EVALUATED ✓                │
│  counter.label = "X"  → body NOT re-evaluated ✗            │
│                                                            │
│  SwiftUI's observation tracking sees that only `count`     │
│  was accessed → only subscribes to `count`.                │
│                                                            │
└────────────────────────────────────────────────────────────┘

Side-by-Side Comparison: Declaration-Based vs Access-Based

┌─────────────── Declaration-Based ──────────────────────────────────────┐
│                                                                         │
│  ObservableObject + Combine                                             │
│                                                                         │
│  class Model: ObservableObject {                                        │
│      @Published var a = 1   ─┐                                          │
│      @Published var b = 2    ├──→ ALL fire objectWillChange              │
│      @Published var c = 3   ─┘    (single signal, no granularity)       │
│  }                                                                      │
│                                                                         │
│  @StateObject / @ObservedObject / @EnvironmentObject var model          │
│                                                                         │
│  body reads only `a`:                                                   │
│      model.b changes → body re-evaluated anyway ⚠️                      │
│      model.c changes → body re-evaluated anyway ⚠️                      │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────── Access-Based ───────────────────────────────────────────┐
│                                                                         │
│  @Observable (Observation framework, iOS 17+)                           │
│                                                                         │
│  @Observable                                                            │
│  class Model {                                                          │
│      var a = 1   ← tracked individually                                 │
│      var b = 2   ← tracked individually                                 │
│      var c = 3   ← tracked individually                                 │
│  }                                                                      │
│                                                                         │
│  @Bindable var model  (or just `var model`)                             │
│                                                                         │
│  body reads only `a`:                                                   │
│      model.b changes → body NOT re-evaluated ✓ (efficient)              │
│      model.c changes → body NOT re-evaluated ✓ (efficient)              │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Complete Example: All Property Wrappers in Action

import SwiftUI
import Observation

// ──────────────────────────────────────────────
// MARK: - Combine-based (Declaration-Based)
// ──────────────────────────────────────────────

class SessionManager: ObservableObject {
    @Published var isLoggedIn = false
    @Published var username = "Guest"
    @Published var tokenExpiry = Date()
}

class ThemeSettings: ObservableObject {
    @Published var accentColor = Color.blue
    @Published var cornerRadius: CGFloat = 8
}

// ──────────────────────────────────────────────
// MARK: - Observation-based (Access-Based)
// ──────────────────────────────────────────────

@Observable
class UserProfile {
    var displayName = "Alice"
    var bio = "iOS Developer"
    var followerCount = 42
    var avatarURL: URL? = nil
}

// ──────────────────────────────────────────────
// MARK: - Root View
// ──────────────────────────────────────────────

struct RootView: View {
    // @StateObject — owns the object, declaration-based
    @StateObject private var session = SessionManager()
    @StateObject private var theme = ThemeSettings()

    // @State — simple value, access-based
    @State private var tabIndex = 0

    var body: some View {
        TabView(selection: $tabIndex) {
            HomeTab()
                .tabItem { Label("Home", systemImage: "house") }
                .tag(0)

            ProfileTab(profile: UserProfile())
                .tabItem { Label("Profile", systemImage: "person") }
                .tag(1)
        }
        // Inject into environment for any descendant to use
        .environmentObject(session)
        .environmentObject(theme)
    }
}

// ──────────────────────────────────────────────
// MARK: - @EnvironmentObject usage
// ──────────────────────────────────────────────

struct HomeTab: View {
    // @EnvironmentObject — declaration-based
    // ANY @Published change on session → this body re-evaluates
    @EnvironmentObject var session: SessionManager

    // @Environment — declaration-based (single key)
    @Environment(\.colorScheme) var colorScheme

    var body: some View {
        VStack {
            Text("Welcome, \(session.username)")
                .foregroundStyle(colorScheme == .dark ? .white : .black)

            LoginButton(isLoggedIn: $session.isLoggedIn)
        }
    }
}

// ──────────────────────────────────────────────
// MARK: - @Binding usage
// ──────────────────────────────────────────────

struct LoginButton: View {
    // @Binding — declaration-based
    // Connected to parent's session.isLoggedIn
    // LoginButton (Child): Re-evaluates only when the specific value passed into its @Binding changes (Value-level diffing).
    @Binding var isLoggedIn: Bool

    var body: some View {
        Button(isLoggedIn ? "Log Out" : "Log In") {
            isLoggedIn.toggle()
        }
    }
}

// ──────────────────────────────────────────────
// MARK: - @Bindable usage (Access-Based)
// ──────────────────────────────────────────────

struct ProfileTab: View {
    // @Bindable — access-based (Observation framework)
    // Only properties READ in body create subscriptions
    @Bindable var profile: UserProfile

    var body: some View {
        VStack(spacing: 16) {
            // $profile.displayName passes a Binding — does NOT read the value
            TextField("Name", text: $profile.displayName)

            // $profile.bio passes a Binding — does NOT read the value
            TextField("Bio", text: $profile.bio)

            // ProfileTab.body itself reads NO properties on `profile`,
            // so mutating profile.displayName, profile.bio, etc.
            // will NOT invalidate ProfileTab.body.
            // (TextField's own body reads the binding internally —
            //  that's where the subscription lives.)
        }
        .padding()
    }
}

// ──────────────────────────────────────────────
// MARK: - @Observable without wrapper (Access-Based)
// ──────────────────────────────────────────────

struct FollowerCountBadge: View {
    // Plain property — access-based tracking is automatic
    var profile: UserProfile

    var body: some View {
        // Only reads `followerCount` → only subscribes to that
        Text("\(profile.followerCount) followers")
            .font(.caption)
            .padding(6)
            .background(Capsule().fill(.blue.opacity(0.2)))
    }
    // profile.displayName changes → NOT re-evaluated
    // profile.followerCount changes → RE-EVALUATED
}

Dependency map for the example above:

┌─── Declaration-Based Dependencies ──────────────────────────────────────┐
│                                                                          │
│  SessionManager.objectWillChange ──→ RootView.body (@StateObject owner)  │
│  SessionManager.objectWillChange ──→ HomeTab.body (@EnvironmentObject)   │
│  ThemeSettings.objectWillChange  ──→ RootView.body (@StateObject owner)  │
│  ThemeSettings.objectWillChange  ──→ (any view with @EnvironmentObject)  │
│  \.colorScheme                   ──→ HomeTab.body (declared key)          │
│  session.isLoggedIn (Binding)    ──→ LoginButton.body (parent pushes)    │
│                                                                          │
└──────────────────────────────────────────────────────────────────────────┘

┌─── Access-Based Dependencies ───────────────────────────────────────────┐
│                                                                          │
│  @State tabIndex             ──→ (not read — only $tabIndex binding      │
│                                   is passed, so NO dependency registered)│
│  profile.displayName         ──→ TextField.body (TextField reads the     │
│                                   binding internally, NOT ProfileTab)    │
│  profile.bio                 ──→ TextField.body (same as above)          │
│  profile.followerCount       ──→ FollowerCountBadge.body                 │
│                                                                          │
│  profile.avatarURL           ──→ (nobody reads it → no subscriber)       │
│                                                                          │
└──────────────────────────────────────────────────────────────────────────┘

The Diff Mechanism — When Does Re-rendering Happen?

SwiftUI does NOT re-render the entire view tree on every state change. It uses a multi-level diff process:

Step 1: Invalidation

When a dynamic property changes, SwiftUI marks only the views whose body subscribed to it as “dirty” (needing re-evaluation). The granularity is at the view level — the entire body is re-evaluated, not individual subviews.

count changes: 0 → 1

Attribute Graph:
  ┌────────────────────────────────────────────────────────┐
  │  @State count ──→ subscriber: CounterView.body         │
  │                         │                               │
  │                         ▼                               │
  │               mark CounterView as DIRTY                 │
  │               (its body needs re-evaluation)            │
  └────────────────────────────────────────────────────────┘

Step 2: Body Re-evaluation

SwiftUI calls body only on invalidated views, not the entire tree.

View Hierarchy:

     AppView.body          ← NOT called (no dependency changed)
        │
     ProfileView.body      ← CALLED (count changed, it's a subscriber)
        │
        ├── VStack         ← structural container, always re-evaluated with parent
        │    ├── Text      ← new value: "Count: 1"
        │    └── Button    ← unchanged
        │
     OtherView.body        ← NOT called (no dependency changed)

Step 3: Structural Diff (View Tree Comparison)

After re-evaluating body, SwiftUI compares the new view description with the previous one:

// Previous body result:           // New body result:
VStack {                            VStack {
    Text("Count: 0")     ──diff──→     Text("Count: 1")     VALUE changed
    Button("+1") { ... }               Button("+1") { ... }  IDENTICAL (skip)
}                                   }

Diff rules:

Comparison Result Action
Same type, same identity Check properties Update if properties differ
Same type, different identity Treat as different view Remove old, insert new
Different type Different view Remove old, insert new
Identical (value equality) No change Skip entirely — no render update

Step 4: Render Tree Update

Only the actually changed nodes are sent to the render layer:

Diff result:  Text("Count: 0") → Text("Count: 1")
                                     │
                                     ▼
              ┌──────────────────────────────────┐
              │  Render layer:                    │
              │  Update text node content only    │
              │  Position, size, color unchanged  │
              │  → minimal GPU/CoreAnimation work │
              └──────────────────────────────────┘

Full Lifecycle — Putting It All Together

┌─────────────────────────────────────────────────────────────────────────┐
│                        SwiftUI Update Cycle                              │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  1. STATE CHANGE                                                         │
│     ┌───────────┐                                                        │
│     │ count = 1 │  (user taps button)                                    │
│     └─────┬─────┘                                                        │
│           │                                                              │
│  2. INVALIDATION                                                         │
│           │                                                              │
│           ▼                                                              │
│     ┌─────────────────────────────────────┐                              │
│     │ Look up subscribers of `count`      │                              │
│     │ Mark them as dirty                  │                              │
│     └─────────────────┬───────────────────┘                              │
│                       │                                                  │
│  3. RE-EVALUATE BODY (only dirty views)                                  │
│                       │                                                  │
│                       ▼                                                  │
│     ┌─────────────────────────────────────┐                              │
│     │ Call body on CounterView            │                              │
│     │ Track new dependency subscriptions  │                              │
│     │ Produce new view description        │                              │
│     └─────────────────┬───────────────────┘                              │
│                       │                                                  │
│  4. DIFF                                                                 │
│                       │                                                  │
│                       ▼                                                  │
│     ┌─────────────────────────────────────┐                              │
│     │ Compare old tree vs new tree        │                              │
│     │ Text("Count: 0") ≠ Text("Count: 1")│                              │
│     │ Button("+1") == Button("+1") ✓ skip │                              │
│     └─────────────────┬───────────────────┘                              │
│                       │                                                  │
│  5. RENDER UPDATE (minimal)                                              │
│                       │                                                  │
│                       ▼                                                  │
│     ┌─────────────────────────────────────┐                              │
│     │ Patch only Text node in render tree │                              │
│     │ Commit to display                   │                              │
│     └─────────────────────────────────────┘                              │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Why This Matters — Performance Implications

struct ParentView: View {
    @State private var counter = 0

    var body: some View {
        VStack {
            Text("Counter: \(counter)")      // ← re-evaluated (reads counter)
            Button("+1") { counter += 1 }

            ExpensiveChildView()              // ← body NOT called if it doesn't
                                             //   read `counter`
        }
    }
}

struct ExpensiveChildView: View {
    // No dependency on parent's @State
    var body: some View {
        // Complex view tree...
        Text("I'm expensive but I don't re-render!")
    }
}

Key insight: ExpensiveChildView.body is not re-called when counter changes because it has no subscription to that property. SwiftUI’s dependency tracking ensures only relevant views are updated.

counter changes: 0 → 1

  ParentView.body ← CALLED (subscriber of counter)
      │
      ├── Text("Counter: 1")       ← updated
      ├── Button                    ← diffed, unchanged, skip
      └── ExpensiveChildView        ← struct re-created (value type)
                                       BUT .body NOT called
                                       (no dependency on counter)
                                       SwiftUI compares struct fields:
                                       no inputs changed → skip body entirely

Section 7: Identity in SwiftUI

SwiftUI uses identity to track views across re-evaluations. Identity determines whether a view appearing in a new body result is the same view as before (and should preserve its state) or a different view (and should be created fresh). There are two forms of identity:


Explicit Identity

Explicit identity is assigned by the developer using the .id() modifier or through data-driven constructs like ForEach with Identifiable items. It tells SwiftUI: “this view corresponds to this specific identifier.”

struct ExplicitIdentityDemo: View {
    @State private var activeTab = 0
    @State private var messages: [Message] = [
        Message(id: "msg-001", text: "Hello"),
        Message(id: "msg-002", text: "World"),
        Message(id: "msg-003", text: "SwiftUI"),
    ]

    var body: some View {
        VStack(spacing: 20) {

            // ─── Explicit identity via .id() modifier ───
            // Changing the id forces SwiftUI to DESTROY the old view
            // and CREATE a new one (resetting all internal state)
            ScrollViewReader { proxy in
                ScrollView {
                    Text("Content for tab \(activeTab)")
                        .id(activeTab)  // ← explicit identity
                        // When activeTab changes: old Text is destroyed,
                        // new Text is created (animations, transitions,
                        // and internal state all reset)
                }
            }

            // ─── Explicit identity via ForEach + Identifiable ───
            // Each Message is identified by its `id` property.
            // SwiftUI tracks insertions, deletions, and reordering
            // by matching identifiers across re-evaluations.
            List {
                ForEach(messages) { message in
                    // identity = message.id ("msg-001", "msg-002", etc.)
                    MessageRow(message: message)
                }
                .onDelete { offsets in
                    messages.remove(atOffsets: offsets)
                }
            }

            Button("Switch Tab") {
                activeTab += 1
            }
        }
    }
}

struct Message: Identifiable {
    let id: String
    var text: String
}

struct MessageRow: View {
    let message: Message
    @State private var isExpanded = false  // state tied to identity

    var body: some View {
        Text(message.text)
            .onTapGesture { isExpanded.toggle() }
    }
}

Annotated Layout Mockup:

┌─── ExplicitIdentityDemo ───────────────────────────────────────────────┐
│                                                                         │
│  ┌─── ScrollView ────────────────────────────────────────────────────┐ │
│  │                                                                    │ │
│  │   Text("Content for tab 0")                                       │ │
│  │        .id(activeTab)  ← identity = 0                             │ │
│  │                                                                    │ │
│  │   When activeTab changes 0 → 1:                                   │ │
│  │     • SwiftUI sees .id(0) is GONE, .id(1) is NEW                  │ │
│  │     • Old Text (id=0) is DESTROYED (state lost)                   │ │
│  │     • New Text (id=1) is CREATED (fresh state)                    │ │
│  │     • Transition animations play (remove old, insert new)         │ │
│  │                                                                    │ │
│  └────────────────────────────────────────────────────────────────────┘ │
│                                                                         │
│  ┌─── List + ForEach (identity = Message.id) ────────────────────────┐ │
│  │                                                                    │ │
│  │   ┌──────────────────────────────────┐  identity: "msg-001"       │ │
│  │   │ "Hello"                          │  @State isExpanded is      │ │
│  │   └──────────────────────────────────┘  tied to this identity     │ │
│  │   ┌──────────────────────────────────┐  identity: "msg-002"       │ │
│  │   │ "World"                          │                            │ │
│  │   └──────────────────────────────────┘                            │ │
│  │   ┌──────────────────────────────────┐  identity: "msg-003"       │ │
│  │   │ "SwiftUI"                        │                            │ │
│  │   └──────────────────────────────────┘                            │ │
│  │                                                                    │ │
│  │   If list reorders (msg-003 moves to top):                        │ │
│  │     • SwiftUI matches by id, NOT by position                      │ │
│  │     • "msg-003" keeps its @State (isExpanded)                     │ │
│  │     • Animation shows the row sliding up                          │ │
│  │                                                                    │ │
│  └────────────────────────────────────────────────────────────────────┘ │
│                                                                         │
│  ┌──────────────────┐                                                   │
│  │   Switch Tab      │                                                   │
│  └──────────────────┘                                                   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Key insight — .id() as a state reset tool:

  activeTab: 0               activeTab: 1
  ┌─────────────────┐        ┌─────────────────┐
  │ Text (id=0)     │  ──X   │ Text (id=1)     │  ← brand new instance
  │ state: { ... }  │  GONE  │ state: { }      │    (state reset)
  └─────────────────┘        └─────────────────┘

Structural Identity

Structural identity is determined by a view’s position in the view hierarchy (its path in the body’s result tree). SwiftUI uses the type and position of a view to identify it when no explicit identity is given. This means views in different branches of if/else are different views with distinct identities.

struct StructuralIdentityDemo: View {
    @State private var isLoggedIn = false

    var body: some View {
        VStack(spacing: 20) {

            // ─── if/else = TWO DIFFERENT views (structural identity) ───
            // The view in the `if` branch and the view in the `else` branch
            // have DIFFERENT structural identities. Toggling `isLoggedIn`
            // DESTROYS one and CREATES the other (state does NOT transfer).
            if isLoggedIn {
                // Structural identity: VStack/[0]/ConditionalContent/TrueContent
                WelcomeView()
            } else {
                // Structural identity: VStack/[0]/ConditionalContent/FalseContent
                LoginPromptView()
            }

            // ─── Same position, same type = SAME view (preserved state) ───
            // Both branches produce a Text at the same structural position
            // with the same type. SwiftUI treats it as the SAME view —
            // only the parameters change, state is PRESERVED.
            Text(isLoggedIn ? "Welcome back!" : "Please log in")
                .font(isLoggedIn ? .title : .body)
                // This is ONE view that gets updated, not two different views.
                // Internal state (if any) would be preserved.

            Button(isLoggedIn ? "Log Out" : "Log In") {
                withAnimation {
                    isLoggedIn.toggle()
                }
            }
        }
    }
}

struct WelcomeView: View {
    @State private var animationProgress: CGFloat = 0

    var body: some View {
        VStack {
            Text("Welcome!")
                .font(.largeTitle)
            ProgressView(value: animationProgress)
                .onAppear { animationProgress = 0.8 }
        }
    }
}

struct LoginPromptView: View {
    @State private var shake = false

    var body: some View {
        VStack {
            Image(systemName: "lock.fill")
                .font(.largeTitle)
            Text("Sign in to continue")
        }
    }
}

Annotated Layout Mockup:

┌─── StructuralIdentityDemo.body ────────────────────────────────────────┐
│                                                                         │
│  VStack                                                                 │
│  ├── [position 0]: ConditionalContent (if/else)                        │
│  ├── [position 1]: Text                                                │
│  └── [position 2]: Button                                              │
│                                                                         │
│  ═══════════════════════════════════════════════════════════════════════ │
│                                                                         │
│  When isLoggedIn = false:                                               │
│  ┌────────────────────────────────────────────────────────────────────┐ │
│  │ ConditionalContent → FalseContent branch                           │ │
│  │ ┌──────────────────────────────────────────┐                       │ │
│  │ │ LoginPromptView                          │                       │ │
│  │ │   🔒                                     │                       │ │
│  │ │   "Sign in to continue"                  │                       │ │
│  │ │   @State shake = false (own state)       │                       │ │
│  │ └──────────────────────────────────────────┘                       │ │
│  └────────────────────────────────────────────────────────────────────┘ │
│  ┌────────────────────────────────────────┐                             │
│  │ "Please log in"  (.body font)          │ ← same Text view always    │
│  └────────────────────────────────────────┘                             │
│  ┌──────────┐                                                           │
│  │  Log In  │                                                           │
│  └──────────┘                                                           │
│                                                                         │
│  ═══════════════════════════════════════════════════════════════════════ │
│                                                                         │
│  Toggle isLoggedIn → true:                                              │
│  ┌────────────────────────────────────────────────────────────────────┐ │
│  │ ConditionalContent → TrueContent branch                            │ │
│  │ ┌──────────────────────────────────────────┐                       │ │
│  │ │ WelcomeView        ← BRAND NEW instance  │                       │ │
│  │ │   "Welcome!"                             │                       │ │
│  │ │   ProgressView                           │                       │ │
│  │ │   @State animationProgress = 0 (fresh)   │                       │ │
│  │ └──────────────────────────────────────────┘                       │ │
│  │                                                                    │ │
│  │ LoginPromptView is DESTROYED                                       │ │
│  │   (its @State shake is gone forever)                               │ │
│  └────────────────────────────────────────────────────────────────────┘ │
│  ┌────────────────────────────────────────┐                             │
│  │ "Welcome back!"  (.title font)         │ ← same Text, updated      │
│  └────────────────────────────────────────┘                             │
│  ┌──────────┐                                                           │
│  │  Log Out │                                                           │
│  └──────────┘                                                           │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Structural identity tree:

  VStack
  ├── ConditionalContent        ← compiler turns if/else into this
  │   ├── TrueContent:  WelcomeView       ← identity A
  │   └── FalseContent: LoginPromptView   ← identity B (different!)
  │
  ├── Text(...)                 ← identity C (always the same view,
  │                                regardless of the string content)
  └── Button(...)              ← identity D

  Key rule: Same type + same position = same identity = state preserved
            Different branch of if/else = different identity = state reset

Section 8: Bridging UIKit and SwiftUI


Using a UIKit View in SwiftUI — UIViewRepresentable

Wrap any UIView subclass so it can be used as a SwiftUI view. You implement makeUIView (creation) and updateUIView (sync SwiftUI state → UIKit).

Context is a struct (value type). Its full type is UIViewRepresentable.Context, which is a typealias for UIViewRepresentableContext.

Who Owns It?

SwiftUI owns it. You never create it yourself — the framework constructs it and passes it to you as a parameter:

func makeUIView(context: Context) -> UIView      // SwiftUI gives it to you
func updateUIView(_ uiView: UIView, context: Context)  // SwiftUI gives it to you

Where Is It Stored?

It's not stored anywhere you can access. SwiftUI constructs it on-the-fly each time it calls your methods. It bundles together:

- A reference to your Coordinator (which SwiftUI stores internally and keeps alive for the view's lifetime)
- A copy of the current EnvironmentValues (which flows down the view tree)
- The current Transaction (animation state for this update pass)

Mental Model

Think of Context as a read-only briefcase SwiftUI hands you each time it calls your functions. You open it, take what you need, and you're done. You don't keep it,
 you don't mutate it, you don't create it.

SwiftUI (owner of everything)
├── Creates & stores Coordinator (long-lived, class)
├── Holds EnvironmentValues (value type, flows down)
├── Holds current Transaction (value type, per-update)
│
└── On each call: bundles these into Context (struct) → passes to you

The Coordinator inside it is a class (reference type, long-lived). The Context wrapper itself is a struct (value type, ephemeral).
import SwiftUI
import UIKit
import MapKit

// ─── Wrapping MKMapView (UIKit) for use in SwiftUI ───

struct MapView: UIViewRepresentable {
    // SwiftUI state flowing IN
    var centerCoordinate: CLLocationCoordinate2D
    var zoomLevel: Double

    // ─── 1. Create the UIKit view ───
    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.delegate = context.coordinator  // connect delegate
        return mapView
    }

    // ─── 2. Update UIKit view when SwiftUI state changes ───
    func updateUIView(_ mapView: MKMapView, context: Context) {
        let region = MKCoordinateRegion(
            center: centerCoordinate,
            span: MKCoordinateSpan(
                latitudeDelta: zoomLevel,
                longitudeDelta: zoomLevel
            )
        )
        mapView.setRegion(region, animated: true)
    }

    // ─── 3. Coordinator handles UIKit delegates/callbacks → SwiftUI ───
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, MKMapViewDelegate {
        var parent: MapView

        init(_ parent: MapView) {
            self.parent = parent
        }

        func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
            // UIKit callback → can update SwiftUI state via parent
            // e.g., parent.centerCoordinate = mapView.centerCoordinate
        }
    }
}

// ─── Usage in SwiftUI ───

struct ContentView: View {
    @State private var center = CLLocationCoordinate2D(
        latitude: 37.7749, longitude: -122.4194
    )
    @State private var zoom = 0.05

    var body: some View {
        VStack {
            // Use the UIKit MKMapView as if it were a native SwiftUI view
            MapView(centerCoordinate: center, zoomLevel: zoom)
                .frame(height: 300)
                .cornerRadius(12)

            Slider(value: $zoom, in: 0.01...1.0)
                .padding()

            Text("Zoom: \(zoom, specifier: "%.3f")")
        }
    }
}

Annotated Layout Mockup:

┌─── ContentView (SwiftUI) ──────────────────────────────────────────────┐
│                                                                         │
│  ┌─── MapView: UIViewRepresentable ──────────────────────────────────┐ │
│  │                                                                    │ │
│  │   ┌────────────────────────────────────────────────────────────┐  │ │
│  │   │                                                            │  │ │
│  │   │         MKMapView (UIKit)                                  │  │ │
│  │   │         rendered natively inside SwiftUI                   │  │ │
│  │   │                                                            │  │ │
│  │   │                    📍 San Francisco                        │  │ │
│  │   │                                                            │  │ │
│  │   └────────────────────────────────────────────────────────────┘  │ │
│  │                                                                    │ │
│  │   Data flow:                                                       │ │
│  │   SwiftUI @State ──→ updateUIView() ──→ MKMapView properties      │ │
│  │   MKMapView delegate ──→ Coordinator ──→ (can update @State)      │ │
│  │                                                                    │ │
│  └────────────────────────────────────────────────────────────────────┘ │
│                                                                         │
│  ┌────────────────────────────────────────────────────────────────────┐ │
│  │  ──────────●────────────────────  Slider (zoom)                    │ │
│  └────────────────────────────────────────────────────────────────────┘ │
│                                                                         │
│  ┌────────────────────────────────────────────────────────────────────┐ │
│  │  "Zoom: 0.050"                                                     │ │
│  └────────────────────────────────────────────────────────────────────┘ │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

UIViewRepresentable lifecycle:

  SwiftUI re-evaluates body
       │
       ▼
  ┌─────────────────────────┐     (first time)     ┌──────────────────┐
  │  centerCoordinate       │ ──────────────────→  │  makeUIView()    │
  │  zoomLevel              │                      │  (create once)   │
  └─────────────────────────┘                      └──────────────────┘
       │
       ▼ (subsequent updates)
  ┌─────────────────────────┐
  │  updateUIView()         │ ← called every time SwiftUI state changes
  │  sync state → UIKit     │
  └─────────────────────────┘

Using a SwiftUI View in UIKit — UIHostingController

Embed any SwiftUI view inside a UIKit view hierarchy using UIHostingController. It acts as a UIViewController whose content is a SwiftUI view.

import SwiftUI
import UIKit

// ─── A SwiftUI view to embed ───

struct ProfileBadge: View {
    let username: String
    @State private var isExpanded = false

    var body: some View {
        VStack(spacing: 8) {
            Image(systemName: "person.crop.circle.fill")
                .font(.system(size: 60))
                .foregroundStyle(.blue)

            Text(username)
                .font(.headline)

            if isExpanded {
                Text("iOS Developer | SwiftUI Enthusiast")
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }

            Button(isExpanded ? "Less" : "More") {
                withAnimation { isExpanded.toggle() }
            }
            .font(.caption)
        }
        .padding()
        .background(RoundedRectangle(cornerRadius: 12).fill(.ultraThinMaterial))
    }
}

// ─── UIKit ViewController embedding the SwiftUI view ───

class ProfileViewController: UIViewController {

    private var hostingController: UIHostingController<ProfileBadge>!

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        title = "Profile"

        // 1. Create the hosting controller with a SwiftUI view
        let swiftUIView = ProfileBadge(username: "Alice")
        hostingController = UIHostingController(rootView: swiftUIView)

        // 2. Add as child view controller
        addChild(hostingController)
        view.addSubview(hostingController.view)
        hostingController.didMove(toParent: self)

        // 3. Set up constraints (treat it like any UIView)
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            hostingController.view.centerXAnchor.constraint(
                equalTo: view.centerXAnchor),
            hostingController.view.centerYAnchor.constraint(
                equalTo: view.centerYAnchor),
            hostingController.view.leadingAnchor.constraint(
                greaterThanOrEqualTo: view.leadingAnchor, constant: 20),
            hostingController.view.trailingAnchor.constraint(
                lessThanOrEqualTo: view.trailingAnchor, constant: -20),
        ])
    }

    // ─── Update the SwiftUI view from UIKit side ───
    func updateUsername(_ name: String) {
        hostingController.rootView = ProfileBadge(username: name)
    }
}

// ─── Also works inline with SwiftUI views in UIKit navigation ───

class AppCoordinator {
    let navigationController: UINavigationController

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func showSettings() {
        // Directly push a SwiftUI view onto a UIKit navigation stack
        let settingsView = SettingsView()
        let hostingVC = UIHostingController(rootView: settingsView)
        hostingVC.title = "Settings"
        navigationController.pushViewController(hostingVC, animated: true)
    }
}

struct SettingsView: View {
    @State private var notificationsOn = true
    @State private var darkMode = false

    var body: some View {
        Form {
            Toggle("Notifications", isOn: $notificationsOn)
            Toggle("Dark Mode", isOn: $darkMode)
        }
    }
}

Annotated Layout Mockup:

┌─── ProfileViewController (UIKit) ─────────────────────────────────────┐
│                                                                         │
│  UINavigationBar: "Profile"                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐│
│  │                                                                     ││
│  │                                                                     ││
│  │         ┌─── UIHostingController.view ───────────────┐              ││
│  │         │                                            │              ││
│  │         │  ┌─── ProfileBadge (SwiftUI) ───────────┐  │              ││
│  │         │  │                                      │  │              ││
│  │         │  │         👤 (60pt icon)               │  │              ││
│  │         │  │                                      │  │              ││
│  │         │  │         "Alice"                      │  │              ││
│  │         │  │          (.headline)                 │  │              ││
│  │         │  │                                      │  │              ││
│  │         │  │         [More]                       │  │              ││
│  │         │  │                                      │  │              ││
│  │         │  └──────────────────────────────────────┘  │              ││
│  │         │   .background(RoundedRectangle + material) │              ││
│  │         └────────────────────────────────────────────┘              ││
│  │                                                                     ││
│  │         Constraints: centerX, centerY, leading ≥ 20, trailing ≤ -20 ││
│  │                                                                     ││
│  └─────────────────────────────────────────────────────────────────────┘│
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

UIHostingController integration pattern:

  ┌──────────────────────────────────────────────────────────────────────┐
  │                                                                      │
  │  UIKit World                          SwiftUI World                  │
  │                                                                      │
  │  UIViewController                     View (struct)                  │
  │       │                                    ▲                         │
  │       │  addChild(hostingVC)               │                         │
  │       │  view.addSubview(hostingVC.view)   │                         │
  │       ▼                                    │                         │
  │  UIHostingController ─── rootView: ────────┘                         │
  │       │                                                              │
  │       │  Bridges:                                                    │
  │       │  • UIKit layout (Auto Layout) ↔ SwiftUI layout              │
  │       │  • UIKit lifecycle ↔ SwiftUI lifecycle (.onAppear, etc.)     │
  │       │  • UIKit traits ↔ SwiftUI Environment                       │
  │       │                                                              │
  │       │  To update SwiftUI from UIKit:                               │
  │       │    hostingVC.rootView = NewView(newProps)                     │
  │       │                                                              │
  └──────────────────────────────────────────────────────────────────────┘

ZijunHuang

Ripley's Blog