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 radiusSwap 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.
| SwiftUI | Figma equivalent | What it does |
|---|---|---|
VStack | Vertical auto-layout | Stacks children top to bottom |
HStack | Horizontal auto-layout | Stacks children left to right |
ZStack | Overlapping layers | Layers 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
| Pattern | State type | Example |
|---|---|---|
| Toggle on/off | @State var isOn: Bool | Dark mode, like button, switch |
| Text input | @State var text: String | Search bar, form field |
| Selection | @State var selected: Int | Tab bar, segmented control |
| Multi-step flow | @State var step: Int | Onboarding, 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
| Curve | Feel | Use for |
|---|---|---|
.easeInOut | Smooth default | Most transitions |
.spring() | Bouncy, physical | Cards, toggles, drags |
.spring(response: 0.3, dampingFraction: 0.6) | Tunable spring | Custom feel |
.easeIn(duration: 0.2) | Accelerates in | Exits, dismissals |
.linear | Constant speed | Progress 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
| Component | What it gives you |
|---|---|
List | Scrollable, grouped rows with swipe actions — settings screens, feeds |
ScrollView | Free-form scrollable area (horizontal or vertical) |
TextField | Text input with keyboard handling |
Toggle | On/off switch with built-in state binding |
Slider | Value slider with customizable range |
ProgressView | Loading 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
| Shortcut | Action |
|---|---|
| ⌘R | Run on simulator |
| ⌘. | Stop running |
| ⌥⌘P | Resume the live preview canvas |
| ⌘B | Build (check for errors without running) |
| ⌘⇧L | Open 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
#Previewmacro 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
| Week | Focus | Build |
|---|---|---|
| 1 | Layout + State | A settings screen with toggleable sections |
| 2 | Animation + Transitions | An onboarding flow with animated page transitions |
| 3 | Gestures + Drag | A bottom sheet that snaps to three heights |
| 4 | Navigation + Composition | A 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.