🍎 SwiftUI Cheat Sheet
Views, State, ObservableObject, NavigationStack, animations and more.
🧩 Core Views
Basic view structure
struct ContentView: View {
var body: some View {
Text("Hello, SwiftUI!")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.blue)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(12)
}
}Text modifiers
Text("Hello")
.font(.system(size: 16, weight: .semibold, design: .rounded))
.foregroundStyle(.primary)
.multilineTextAlignment(.center)
.lineSpacing(4)
.kerning(0.5)
.lineLimit(2)
.truncationMode(.tail)Images
Image("logo")
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
.clipShape(Circle())
.overlay(Circle().stroke(Color.blue, lineWidth: 2))
// SF Symbols
Image(systemName: "heart.fill")
.imageScale(.large)
.symbolRenderingMode(.multicolor)
.font(.system(size: 24))
// Async image
AsyncImage(url: URL(string: "https://...")) { phase in
switch phase {
case .empty: ProgressView()
case .success(let img): img.resizable().scaledToFill()
case .failure: Image(systemName: "photo")
@unknown default: EmptyView()
}
}
.frame(width: 80, height: 80)
.clipShape(RoundedRectangle(cornerRadius: 12))Buttons
Button("Tap me") { handleTap() }
Button {
handleTap()
} label: {
Label("Save", systemImage: "square.and.arrow.down")
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
.buttonStyle(.plain)
// Link
Link("Visit website", destination: URL(string: "https://apple.com")!)🔄 State & Binding
@State — local state
struct CounterView: View {
@State private var count = 0
@State private var isShowing = false
@State private var text = ""
var body: some View {
VStack {
Text("\(count)")
Button("+") { count += 1 }
TextField("Name", text: $text)
Toggle("Show", isOn: $isShowing)
}
}
}@Binding — pass state down
struct ChildView: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Feature", isOn: $isOn)
}
}
struct ParentView: View {
@State private var featureEnabled = false
var body: some View {
ChildView(isOn: $featureEnabled)
}
}@AppStorage — UserDefaults
struct SettingsView: View {
@AppStorage("isDarkMode") private var isDarkMode = false
@AppStorage("username") private var username = ""
var body: some View {
Toggle("Dark Mode", isOn: $isDarkMode)
}
}📡 Observable / ViewModel
@Observable (iOS 17+)
import Observation
@Observable
class UserViewModel {
var user: User?
var isLoading = false
var error: String?
private let repo: UserRepository
init(repo: UserRepository) { self.repo = repo }
func loadUser(id: String) async {
isLoading = true
do {
user = try await repo.fetchUser(id: id)
} catch {
self.error = error.localizedDescription
}
isLoading = false
}
}
// In view
struct UserView: View {
@State private var vm = UserViewModel(repo: UserRepositoryImpl())
var body: some View {
Group {
if vm.isLoading { ProgressView() }
else if let user = vm.user { UserCard(user: user) }
}
.task { await vm.loadUser(id: "1") }
}
}ObservableObject (iOS 13+)
class UserViewModel: ObservableObject {
@Published var user: User?
@Published var isLoading = false
func load() {
isLoading = true
// ...
}
}
// In view
struct UserView: View {
@StateObject private var vm = UserViewModel()
// or receive from parent:
@ObservedObject var vm: UserViewModel
var body: some View {
Text(vm.user?.name ?? "Loading...")
.onAppear { vm.load() }
}
}
// Share across many views
struct App: View {
@StateObject private var settings = AppSettings()
var body: some View {
ContentView().environmentObject(settings)
}
}
struct DeepView: View {
@EnvironmentObject var settings: AppSettings
}📐 Layout
Stacks
VStack(alignment: .leading, spacing: 12) {
Text("Title").font(.headline)
Text("Subtitle").foregroundColor(.secondary)
}
HStack(alignment: .center, spacing: 8) {
Image(systemName: "star.fill")
Text("Rating")
Spacer() // pushes content to edges
Text("4.5")
}
ZStack(alignment: .bottomTrailing) {
Image("background")
Text("Overlay text")
.padding(8)
.background(.ultraThinMaterial)
.cornerRadius(8)
}Grid & LazyGrid
let columns = [GridItem(.adaptive(minimum: 150))]
ScrollView {
LazyVGrid(columns: columns, spacing: 12) {
ForEach(items) { item in
ItemCard(item: item)
}
}
.padding()
}Frame & padding
Text("Full width")
.frame(maxWidth: .infinity, alignment: .leading)
Text("Fixed")
.frame(width: 100, height: 50)
Text("Min/Max")
.frame(minWidth: 50, maxWidth: 200)
// Padding
.padding() // all sides
.padding(.horizontal, 16) // sides only
.padding(.top, 8) // top only
.padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))📋 Lists
List basics
List {
Section("Recent") {
ForEach(recentItems) { item in
ItemRow(item: item)
.swipeActions(edge: .trailing) {
Button(role: .destructive) { delete(item) } label: {
Label("Delete", systemImage: "trash")
}
Button { pin(item) } label: {
Label("Pin", systemImage: "pin")
}
.tint(.orange)
}
}
.onDelete(perform: deleteItems)
.onMove(perform: moveItems)
}
}
.listStyle(.insetGrouped)
.refreshable { await refreshData() } // pull to refresh✨ Animation
Implicit animations
// withAnimation
Button("Toggle") {
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
isExpanded.toggle()
}
}
// animation modifier
Circle()
.frame(width: isExpanded ? 200 : 100)
.animation(.easeInOut(duration: 0.3), value: isExpanded)
// Transition
if isVisible {
Text("Appearing")
.transition(.asymmetric(
insertion: .move(edge: .trailing).combined(with: .opacity),
removal: .move(edge: .leading).combined(with: .opacity)
))
}Matchedgeometryeffect
@Namespace private var heroNS
// Source
Image(item.image)
.matchedGeometryEffect(id: item.id, in: heroNS)
.onTapGesture { selectedItem = item }
// Destination (in detail view / overlay)
Image(item.image)
.matchedGeometryEffect(id: item.id, in: heroNS)⏳ Async in Views
.task & .onAppear
// .task — auto-cancelled when view disappears
.task {
await vm.loadData()
}
// .task with id — re-runs when id changes
.task(id: selectedUserId) {
await vm.loadUser(id: selectedUserId)
}
// .onAppear / .onDisappear
.onAppear { analytics.trackScreen("Home") }
.onDisappear { vm.cleanup() }
// onChange
.onChange(of: searchText) { _, newValue in
vm.search(query: newValue)
}🌍 Environment
@Environment values
@Environment(\.colorScheme) var colorScheme
@Environment(\.dynamicTypeSize) var typeSize
@Environment(\.dismiss) var dismiss
@Environment(\.presentationMode) var presentationMode
@Environment(\.locale) var locale
@Environment(\.isEnabled) var isEnabled
// Use
if colorScheme == .dark { ... }
dismiss() // dismiss a sheetCustom environment value
struct ThemeKey: EnvironmentKey {
static let defaultValue = AppTheme.default
}
extension EnvironmentValues {
var theme: AppTheme {
get { self[ThemeKey.self] }
set { self[ThemeKey.self] = newValue }
}
}
// Set
ContentView().environment(\.theme, darkTheme)
// Read
@Environment(\.theme) var theme👆 Gestures
Common gestures
// Tap
.onTapGesture(count: 2) { doubleTapped() }
// Long press
.onLongPressGesture(minimumDuration: 0.5) { longPressed() }
// Drag
.gesture(
DragGesture()
.onChanged { value in
offset = value.translation
}
.onEnded { value in
withAnimation { offset = .zero }
}
)
// Gesture priority
.highPriorityGesture(tapGesture)
.simultaneousGesture(longPressGesture)
// Magnification (pinch)
@State private var scale: CGFloat = 1.0
.scaleEffect(scale)
.gesture(
MagnificationGesture()
.onChanged { value in scale = value }
.onEnded { _ in withAnimation { scale = 1.0 } }
)