Skip to content

SwiftUI Notes 1: Basics

28/05/2026

SwiftUI lets you build real, interactive prototypes that feel native — no engineering handoff required. This guide translates SwiftUI's core ideas into design thinking. It covers only what you need to prototype: layout, state, animation, and gesture. Everything else can wait.

I. The Mental Model

1. Think in components, not screens

SwiftUI works the way you already think in Figma. Every rectangle, text layer, and icon is a View. Views nest inside other Views. A card is a View containing an image View, a text View, and a button View. If you've built components with auto-layout, you already understand SwiftUI's composition model.

The key difference: in Figma, a component is a visual template. In SwiftUI, a View is a live description of what should appear on screen. Change the data feeding it, and the UI updates automatically.

2. Declarative = describe, don't instruct

You tell SwiftUI what should be on screen, not how to draw it. This is called declarative programming. Instead of saying "create a label, set its text, position it at x: 20, y: 40," you write:

Text("Hello, world")
    .font(.title)
    .padding()

SwiftUI figures out the rendering. You describe the result. This is closer to how CSS works than how Sketch plugins work — and it's why designers often find SwiftUI more intuitive than UIKit.

3. Modifiers = your properties panel

Every dot-chained method after a View is a modifier. Think of modifiers as the right sidebar in Figma — they change how a View looks or behaves. Order matters: modifiers apply from top to bottom.

Text("Sign Up")
    .font(.headline)           // typography
    .foregroundColor(.white)   // text color
    .padding()                 // inner spacing
    .background(.blue)         // fill color
    .cornerRadius(12)          // corner radius

Swap the order of .padding() and .background() and you get a completely different result. The mental model: each modifier wraps the View in a new layer, like nesting frames in Figma.

II. Layout: Auto-Layout, but Real

1. The three stack types

SwiftUI has three layout containers that map directly to Figma's auto-layout directions.

SwiftUIFigma equivalentWhat it does
VStackVertical auto-layoutStacks children top to bottom
HStackHorizontal auto-layoutStacks children left to right
ZStackOverlapping layersLayers children on top of each other
VStack(spacing: 16) {
    Text("Welcome back")
        .font(.largeTitle)
    Text("You have 3 new messages")
        .font(.body)
        .foregroundColor(.secondary)
    Button("View Inbox") { }
}

That's a vertical stack with 16pt spacing between elements — the same as a Figma frame with vertical auto-layout and 16px item spacing.

2. Spacer and alignment

Spacer() is the SwiftUI equivalent of "fill container" in Figma. It expands to consume all available space, pushing other Views to the edges.

HStack {
    Text("Settings")
        .font(.headline)
    Spacer()  // pushes chevron to the right
    Image(systemName: "chevron.right")
}

For cross-axis alignment, pass it to the stack: VStack(alignment: .leading) is the same as setting your auto-layout's horizontal alignment to "left" in Figma.

3. Padding and frame

.padding() adds internal spacing. .frame() sets explicit dimensions.

// Padding on all sides
Text("Hello").padding()

// Padding on specific edges
Text("Hello").padding(.horizontal, 24)

// Fixed width, flexible height
Text("Hello").frame(width: 200)

// Full width
Text("Hello").frame(maxWidth: .infinity)

.frame(maxWidth: .infinity) is SwiftUI's "fill container" for width — equivalent to setting a Figma frame to "Fill" on horizontal resizing.

III. State: Making Things Interactive

1. @State is your prototype's memory

In Figma, you toggle between variants to show different states. In SwiftUI, you change a variable and the UI rebuilds itself. @State creates a variable that, when changed, causes the View to re-render.

struct LikeButton: View {
    @State var isLiked = false

    var body: some View {
        Button {
            isLiked.toggle()
        } label: {
            Image(systemName: isLiked ? "heart.fill" : "heart")
                .foregroundColor(isLiked ? .red : .gray)
                .font(.title)
        }
    }
}

That's a fully interactive heart button in 12 lines. Tap it and it toggles. No variant swapping, no prototype wiring, no interaction panel. The state variable is the interaction.

2. Conditional rendering

Use if to show and hide Views based on state. This is how you build multi-step flows, expandable cards, error states, and empty states — all the things that require multiple screens or overlays in Figma.

@State var showDetails = false

VStack {
    Button(showDetails ? "Hide" : "Show Details") {
        showDetails.toggle()
    }
    if showDetails {
        Text("Here are the details...")
            .padding()
            .background(Color(.systemGray6))
            .cornerRadius(8)
    }
}

3. Common state patterns for prototypes

PatternState typeExample
Toggle on/off@State var isOn: BoolDark mode, like button, switch
Text input@State var text: StringSearch bar, form field
Selection@State var selected: IntTab bar, segmented control
Multi-step flow@State var step: IntOnboarding, checkout

For prototyping, @State handles 90% of what you'll need. You won't need @Binding, @ObservedObject, or @EnvironmentObject until you're building production apps.

IV. Animation: One Line Changes Everything

1. withAnimation wraps any state change

SwiftUI animates between states automatically. Wrap any state change in withAnimation and SwiftUI interpolates every property that changed — position, opacity, color, scale, all of it.

Button("Toggle") {
    withAnimation(.spring()) {
        isExpanded.toggle()
    }
}

That single wrapper gives you a spring-physics animation on every property that depends on isExpanded. No keyframes, no duration curves, no after-delay wiring. Change the state, get the animation.

2. Animation curves

CurveFeelUse for
.easeInOutSmooth defaultMost transitions
.spring()Bouncy, physicalCards, toggles, drags
.spring(response: 0.3, dampingFraction: 0.6)Tunable springCustom feel
.easeIn(duration: 0.2)Accelerates inExits, dismissals
.linearConstant speedProgress bars, spinners

3. Transitions for enter/exit

When a View appears or disappears, .transition() controls how it enters and exits.

if showCard {
    CardView()
        .transition(.move(edge: .bottom)
            .combined(with: .opacity))
}

Built-in transitions: .opacity, .scale, .slide, .move(edge:). Combine them with .combined(with:). This is where SwiftUI prototypes start to feel like shipping apps — smooth enter/exit choreography with almost zero effort.

V. Gestures: Touch and Drag

1. Tap, long press, and drag

SwiftUI gestures attach directly to Views like modifiers.

// Tap
Circle()
    .onTapGesture { print("tapped") }

// Long press
Circle()
    .onLongPressGesture { print("long pressed") }

// Drag (for cards, sliders, drawers)
@State var offset = CGSize.zero

Circle()
    .offset(offset)
    .gesture(
        DragGesture()
            .onChanged { value in
                offset = value.translation
            }
            .onEnded { _ in
                withAnimation(.spring()) {
                    offset = .zero
                }
            }
    )

The drag example above creates a draggable circle that snaps back on release with spring physics. That's a Tinder-swipe prototype in 10 lines.

2. Gesture-driven prototypes

Combine gestures with state and animation for prototypes you can't build in any design tool. These are the interactions that separate high-fidelity prototypes from clickthrough mockups. Each one takes 20–40 lines of SwiftUI.

  • Swipe-to-dismiss: DragGesture + threshold check + .transition(.move)
  • Pull-to-refresh: DragGesture + offset tracking + spring-back
  • Bottom sheet: DragGesture + snap points + .animation(.spring())
  • Carousel: DragGesture + page index + .offset()

VI. Navigation: Screen Flows

1. NavigationStack and NavigationLink

Wrap your prototype in a NavigationStack to get push/pop navigation for free.

NavigationStack {
    List {
        NavigationLink("Profile") {
            ProfileView()
        }
        NavigationLink("Settings") {
            SettingsView()
        }
    }
    .navigationTitle("Home")
}

Each NavigationLink pushes a new screen with a back button. You get the iOS navigation bar, transitions, and swipe-to-go-back gesture automatically.

2. TabView for tab bars

TabView {
    HomeView()
        .tabItem {
            Image(systemName: "house")
            Text("Home")
        }
    SearchView()
        .tabItem {
            Image(systemName: "magnifyingglass")
            Text("Search")
        }
}

That's a fully functional tab bar with two tabs. Each tab can have its own NavigationStack for independent navigation hierarchies — exactly how real iOS apps work.

3. Sheets and overlays

@State var showSheet = false

Button("Open Sheet") { showSheet = true }
    .sheet(isPresented: $showSheet) {
        SheetContentView()
    }

Sheets slide up from the bottom. .fullScreenCover does the same thing full-screen. For custom overlays, use ZStack with conditional visibility and your own animation.

VII. Your Prototyping Toolkit

ComponentWhat it gives you
ListScrollable, grouped rows with swipe actions — settings screens, feeds
ScrollViewFree-form scrollable area (horizontal or vertical)
TextFieldText input with keyboard handling
ToggleOn/off switch with built-in state binding
SliderValue slider with customizable range
ProgressViewLoading spinners and progress bars
Image(systemName:)800+ SF Symbols icons, built in — no asset imports needed
AsyncImage(url:)Loads remote images with placeholder support

These components come with native iOS styling, dark mode support, and accessibility out of the box. For prototyping, they save hours of manual styling.

VIII. Xcode Survival Guide

1. Getting started

  • Open Xcode → File → New Project → iOS → App
  • Interface: SwiftUI (not Storyboard)
  • Language: Swift
  • Leave everything else as default

Your project opens with ContentView.swift — this is your canvas. The live preview on the right updates as you type. Press ⌘R to run on the simulator.

2. The shortcuts that matter

ShortcutAction
⌘RRun on simulator
⌘.Stop running
⌥⌘PResume the live preview canvas
⌘BBuild (check for errors without running)
⌘⇧LOpen component library (drag-and-drop Views)

3. When errors happen

SwiftUI's compiler errors are notoriously cryptic. Three survival rules:

  • If the error makes no sense, comment out the last thing you added and rebuild. SwiftUI often reports the error in the wrong place.
  • Break large Views into smaller sub-Views. The compiler chokes on complex View hierarchies — extracting pieces fixes phantom errors.
  • Use the #Preview macro at the bottom of your file to see your View in isolation without running the full app.

IX. Your First Prototype: A Card Stack

Paste this into ContentView.swift and hit ⌘R. It builds a swipeable card stack — the kind of interaction that's impossible to prototype in Figma.

import SwiftUI

struct ContentView: View {
    @State private var offset = CGSize.zero
    @State private var currentIndex = 0

    let cards = ["Design Systems", "Motion Design", "Prototyping", "User Research"]

    var body: some View {
        ZStack {
            Color(.systemGroupedBackground)
                .ignoresSafeArea()

            ForEach(cards.indices.reversed(), id: \.self) { i in
                if i >= currentIndex {
                    CardView(title: cards[i])
                        .offset(i == currentIndex ? offset : .zero)
                        .scaleEffect(i == currentIndex ? 1 : 0.95)
                        .opacity(i == currentIndex ? 1 : 0.7)
                        .gesture(
                            i == currentIndex ?
                            DragGesture()
                                .onChanged { value in
                                    offset = value.translation
                                }
                                .onEnded { value in
                                    if abs(value.translation.width) > 120 {
                                        withAnimation(.spring()) {
                                            offset = CGSize(
                                                width: value.translation.width > 0 ? 500 : -500,
                                                height: 0
                                            )
                                        }
                                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
                                            currentIndex += 1
                                            offset = .zero
                                        }
                                    } else {
                                        withAnimation(.spring()) {
                                            offset = .zero
                                        }
                                    }
                                }
                            : nil
                        )
                        .animation(.spring(), value: offset)
                }
            }
        }
    }
}

struct CardView: View {
    let title: String

    var body: some View {
        VStack(spacing: 20) {
            Image(systemName: "sparkles")
                .font(.system(size: 40))
            Text(title)
                .font(.title2)
                .fontWeight(.semibold)
        }
        .frame(width: 300, height: 400)
        .background(.white)
        .cornerRadius(20)
        .shadow(radius: 8, y: 4)
    }
}

#Preview { ContentView() }

This gives you: draggable cards with spring physics, swipe-to-dismiss with a velocity threshold, stacked depth effect via scale and opacity, and automatic progression through a set of cards. Modify the cards array and CardView layout to make it yours.

X. Where to Go Next

WeekFocusBuild
1Layout + StateA settings screen with toggleable sections
2Animation + TransitionsAn onboarding flow with animated page transitions
3Gestures + DragA bottom sheet that snaps to three heights
4Navigation + CompositionA multi-tab app with realistic screen flows

Each project builds on the last. By week 4 you'll be making prototypes that are indistinguishable from shipping apps — and that's the entire point.