🍎 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 sheet
Custom 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 } }
)