SwiftUI Intro
SwiftUI is Apples declarative UI framework: views are structs, state lives in @State / @Observable, and changes trigger a diffed re-render. It targets iOS, iPadOS, macOS, watchOS, tvOS, and visionOS with the same code. The mental model is React-flavoured, with stronger compiler guarantees because everything is statically typed.
A SwiftUI screen with state, navigation, and a list
EXAMPLE
import SwiftUI
// 1) Observable model (Swift 5.9+) — emit changes to any view that reads it
@Observable
final class CartModel {
var items: [Item] = []
var lastError: String?
func add(_ item: Item) { items.append(item) }
func remove(_ item: Item) { items.removeAll { $0.id == item.id } }
var totalCents: Int { items.reduce(0) { $0 + $1.priceCents } }
}
struct Item: Identifiable, Hashable {
let id = UUID()
let name: String
let priceCents: Int
}
// 2) Root view — passes the model through the environment
@main
struct ShopApp: App {
@State private var cart = CartModel()
var body: some Scene {
WindowGroup {
NavigationStack {
CatalogView()
}
.environment(cart)
}
}
}
// 3) Catalog with a list and per-row actions
struct CatalogView: View {
@Environment(CartModel.self) private var cart
@State private var query = ""
private let catalog = [
Item(name: "Wool Jacket", priceCents: 19900),
Item(name: "Linen Shirt", priceCents: 8900),
Item(name: "Beanie", priceCents: 2900),
]
private var filtered: [Item] {
guard !query.isEmpty else { return catalog }
return catalog.filter { $0.name.localizedCaseInsensitiveContains(query) }
}
var body: some View {
List {
ForEach(filtered) { item in
HStack {
Text(item.name)
Spacer()
Text(price(item.priceCents)).monospacedDigit()
Button("Add") { cart.add(item) }.buttonStyle(.bordered)
}
}
}
.searchable(text: $query)
.navigationTitle("Catalog")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
NavigationLink {
CartView()
} label: {
Label("Cart (\(cart.items.count))", systemImage: "cart")
}
}
}
}
}
// 4) Cart view — sheet, swipe-to-delete, sum
struct CartView: View {
@Environment(CartModel.self) private var cart
@Environment(\.dismiss) private var dismiss
var body: some View {
List {
Section("Items") {
ForEach(cart.items) { item in
HStack {
Text(item.name)
Spacer()
Text(price(item.priceCents))
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) { cart.remove(item) }
label: { Label("Remove", systemImage: "trash") }
}
}
}
Section("Summary") {
LabeledContent("Total", value: price(cart.totalCents))
.font(.title3.weight(.semibold))
}
}
.navigationTitle("Cart")
}
}
private func price(_ cents: Int) -> String {
let f = NumberFormatter(); f.numberStyle = .currency; f.currencyCode = "AUD"
return f.string(from: NSNumber(value: Double(cents) / 100)) ?? ""
}
Why it matters
Reach for @Observable + Environment over @ObservableObject + @StateObject — the new macro-based observation system (Swift 5.9+) is finer-grained, only re-renders the views that read the changed property, and removes most of the @Published / @ObservedObject ceremony.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import SwiftUI
struct ContentView: View {
@State private var n = 0
var body: some View {
VStack {
Text("Count: \(n)")
Button("Tap") { n += 1 }
}
.padding()
}
}
Try it Yourself »
Exercise
Declarative view return type.
var body:
View { Text("Hi") }
Four letters.
Discussion
Loading…