Swiftui
SwiftUI declarative UI framework fundamentals for building modern iOS interfaces
You are an expert in SwiftUI for building iOS applications with Swift.
## Key Points
- **`@State`** — Private, view-local mutable state for simple value types.
- **`@Binding`** — Two-way reference to state owned by a parent view.
- **`@StateObject`** — Creates and owns an `ObservableObject` instance (one-time init).
- **`@ObservedObject`** — Observes an `ObservableObject` passed in from outside.
- **`@EnvironmentObject`** — Reads a shared object injected into the environment.
- **`@Environment`** — Reads system-provided environment values (color scheme, locale, etc.).
- **Keep views small and composable.** Extract subviews and use `ViewModifier` for reusable styling. A view's `body` should be easy to read at a glance.
- **Use the right state wrapper.** `@State` for local values, `@StateObject` to own objects, `@ObservedObject` for injected objects. Misuse causes redundant re-creates or lost state.
- **Prefer `@Observable` on iOS 17+.** It is simpler, more performant, and avoids the `@Published` boilerplate of `ObservableObject`.
- **Minimize work in `body`.** The `body` property can be called many times. Avoid heavy computation; move it to view models or use `.task {}`.
- **Use `LazyVStack` / `LazyHStack` inside `ScrollView`.** Eager stacks instantiate all children upfront; lazy stacks instantiate on demand.
- **Leverage previews.** Provide sample data to `#Preview` macros so you can iterate on UI without running the full app.
## Quick Example
```swift
Text("Hello")
.padding() // padding is inside the background
.background(.blue) // background covers the padded area
.padding() // extra padding outside the background
.clipShape(RoundedRectangle(cornerRadius: 8))
```skilldb get ios-swift-skills/SwiftuiFull skill: 289 linesSwiftUI — iOS/Swift
You are an expert in SwiftUI for building iOS applications with Swift.
Core Philosophy
SwiftUI is built on a single powerful idea: the UI is a function of state. You declare what the screen should look like for any given state, and the framework figures out what changed and updates only the affected parts of the view hierarchy. This declarative model eliminates the imperative boilerplate of "when X happens, update Y" that made UIKit code hard to maintain. The developer's job shifts from managing UI mutations to managing state correctly -- and if the state is right, the UI is right.
Views in SwiftUI are lightweight value types, not heavyweight objects. Creating a Text or a VStack is no more expensive than creating an Int -- the framework evaluates your body property, diffs the result against the previous output, and updates only what changed. This means you should not fear creating many small views. Extracting a subview into its own struct is not a performance concern; it is good architecture. The framework rewards composition.
The property wrapper system (@State, @StateObject, @ObservedObject, @Binding, @Environment) is the control surface for SwiftUI's data flow. Using the wrong wrapper causes subtle bugs: @ObservedObject where @StateObject is needed causes the object to be recreated on parent recomposition; @State for reference types does not trigger re-renders. Understanding which wrapper to use and when is not optional -- it is the foundation of correct SwiftUI code.
Anti-Patterns
-
Performing side effects inside the body property: Mutating state, starting network requests, or logging inside
bodycauses infinite re-render loops or unpredictable behavior. Side effects belong in.task {},.onAppear, or event handlers like button actions -- never in the body computation itself. -
Using @ObservedObject when @StateObject is required: If a view creates an
ObservableObjectand stores it as@ObservedObject, the object is destroyed and recreated every time the parent view re-evaluates. Use@StateObjectwhen the view owns the object's lifecycle, and@ObservedObjectonly when the object is passed in from a parent. -
Ignoring modifier order:
.padding().background(.blue)produces a blue background around the padded content, while.background(.blue).padding()produces a blue background only behind the content with transparent padding outside. Each modifier wraps the result of the previous one; the order is the meaning. -
Using eager stacks inside ScrollView: Placing a
VStackwith hundreds of children inside aScrollViewinstantiates all children upfront, even those far off screen. UseLazyVStackorLazyHStackso children are instantiated on demand as they scroll into view. -
Over-observing large models: If a view only needs one property from a large
ObservableObject, it still rebuilds when any@Publishedproperty changes. Break models into smaller observable pieces, use the@Observablemacro on iOS 17+ for fine-grained tracking, or use Combine publishers for specific properties.
Overview
SwiftUI is Apple's declarative UI framework introduced in iOS 13. It replaces the imperative UIKit approach with a reactive, state-driven model where the UI is a function of its state. Views are lightweight value types (structs) that conform to the View protocol, and SwiftUI efficiently diffs and re-renders only what changes.
Core Concepts
Views and the View Protocol
Every UI element in SwiftUI is a struct conforming to View. The single requirement is a computed body property that returns some View.
struct ProfileCard: View {
let username: String
let avatarURL: URL?
var body: some View {
HStack(spacing: 12) {
AsyncImage(url: avatarURL) { image in
image.resizable().scaledToFill()
} placeholder: {
Circle().fill(.gray.opacity(0.3))
}
.frame(width: 48, height: 48)
.clipShape(Circle())
VStack(alignment: .leading) {
Text(username)
.font(.headline)
Text("Online")
.font(.caption)
.foregroundStyle(.green)
}
}
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12))
}
}
State Management
SwiftUI provides several property wrappers to manage data flow:
@State— Private, view-local mutable state for simple value types.@Binding— Two-way reference to state owned by a parent view.@StateObject— Creates and owns anObservableObjectinstance (one-time init).@ObservedObject— Observes anObservableObjectpassed in from outside.@EnvironmentObject— Reads a shared object injected into the environment.@Environment— Reads system-provided environment values (color scheme, locale, etc.).
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 16) {
Text("Count: \(count)")
.font(.largeTitle)
HStack {
Button("Decrement") { count -= 1 }
Button("Increment") { count += 1 }
}
.buttonStyle(.borderedProminent)
}
}
}
Observable Macro (iOS 17+)
The @Observable macro simplifies observation, eliminating the need for @Published and ObservableObject:
@Observable
class SettingsModel {
var isDarkMode = false
var fontSize: Double = 14
var username = ""
}
struct SettingsView: View {
@State private var settings = SettingsModel()
var body: some View {
Form {
Toggle("Dark Mode", isOn: $settings.isDarkMode)
Slider(value: $settings.fontSize, in: 10...24) {
Text("Font Size: \(settings.fontSize, specifier: "%.0f")")
}
TextField("Username", text: $settings.username)
}
}
}
Layout System
SwiftUI uses a three-step layout process: parent proposes size, child reports size, parent places child.
struct DashboardView: View {
var body: some View {
VStack(spacing: 0) {
headerSection
.frame(maxWidth: .infinity)
.background(.blue.gradient)
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible())
], spacing: 16) {
ForEach(0..<4) { index in
MetricCard(index: index)
}
}
.padding()
Spacer()
}
}
private var headerSection: some View {
Text("Dashboard")
.font(.largeTitle.bold())
.foregroundStyle(.white)
.padding()
}
}
View Modifiers
Modifiers return new views wrapping the original. Order matters because each modifier wraps the result of the previous one.
Text("Hello")
.padding() // padding is inside the background
.background(.blue) // background covers the padded area
.padding() // extra padding outside the background
.clipShape(RoundedRectangle(cornerRadius: 8))
Custom view modifiers encapsulate reusable styling:
struct CardModifier: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(.background)
.clipShape(RoundedRectangle(cornerRadius: 12))
.shadow(color: .black.opacity(0.1), radius: 4, y: 2)
}
}
extension View {
func cardStyle() -> some View {
modifier(CardModifier())
}
}
Implementation Patterns
Lists and Scrollable Content
struct ContactListView: View {
@State private var contacts: [Contact] = Contact.samples
@State private var searchText = ""
var filteredContacts: [Contact] {
if searchText.isEmpty { return contacts }
return contacts.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
}
var body: some View {
List {
ForEach(filteredContacts) { contact in
NavigationLink(value: contact) {
ContactRow(contact: contact)
}
}
.onDelete { indexSet in
contacts.remove(atOffsets: indexSet)
}
}
.searchable(text: $searchText, prompt: "Search contacts")
.listStyle(.insetGrouped)
}
}
Sheets, Alerts, and Presentations
struct ItemDetailView: View {
@State private var showDeleteAlert = false
@State private var showEditSheet = false
var body: some View {
VStack {
Button("Edit") { showEditSheet = true }
Button("Delete", role: .destructive) { showDeleteAlert = true }
}
.sheet(isPresented: $showEditSheet) {
EditItemView()
.presentationDetents([.medium, .large])
}
.alert("Delete Item?", isPresented: $showDeleteAlert) {
Button("Cancel", role: .cancel) { }
Button("Delete", role: .destructive) { deleteItem() }
} message: {
Text("This action cannot be undone.")
}
}
}
Animations
struct AnimatedToggle: View {
@State private var isExpanded = false
var body: some View {
VStack {
Button("Toggle") {
withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
isExpanded.toggle()
}
}
RoundedRectangle(cornerRadius: 12)
.fill(.blue.gradient)
.frame(width: isExpanded ? 200 : 80, height: isExpanded ? 200 : 80)
.rotationEffect(.degrees(isExpanded ? 90 : 0))
}
}
}
Best Practices
- Keep views small and composable. Extract subviews and use
ViewModifierfor reusable styling. A view'sbodyshould be easy to read at a glance. - Use the right state wrapper.
@Statefor local values,@StateObjectto own objects,@ObservedObjectfor injected objects. Misuse causes redundant re-creates or lost state. - Prefer
@Observableon iOS 17+. It is simpler, more performant, and avoids the@Publishedboilerplate ofObservableObject. - Minimize work in
body. Thebodyproperty can be called many times. Avoid heavy computation; move it to view models or use.task {}. - Use
LazyVStack/LazyHStackinsideScrollView. Eager stacks instantiate all children upfront; lazy stacks instantiate on demand. - Leverage previews. Provide sample data to
#Previewmacros so you can iterate on UI without running the full app.
Common Pitfalls
- Initializing
@StateObjectwith external parameters ininit. The object may be recreated unexpectedly. Pass dependencies through a factory or use.task {}to configure after creation. - Mutating
@Stateduringbodyevaluation. This triggers an infinite re-render loop. State mutations must happen in event handlers,.onAppear, or.task. - Ignoring modifier order.
.backgroundbefore.paddingyields a different result than.paddingbefore.background. Always reason about what each modifier wraps. - Using
@ObservedObjectwhen you mean@StateObject. If the parent re-creates the child view, an@ObservedObjectwill get a fresh instance, losing state. - Over-observing. If a view only needs one property from a large model, the entire view rebuilds when any published property changes. Break models into smaller observable pieces or use the
@Observablemacro which tracks access automatically.
Install this skill directly: skilldb add ios-swift-skills
Related Skills
App Architecture
MVVM and Clean Architecture patterns for structuring scalable, testable iOS applications
Combine
Combine framework reactive programming patterns for handling asynchronous events in iOS
Core Data
Core Data persistence framework for modeling, storing, and querying structured data in iOS apps
Navigation
SwiftUI navigation patterns using NavigationStack, programmatic routing, and deep linking
Networking
URLSession networking patterns for building robust API clients and handling data transfer in iOS
Swift Concurrency
Swift structured concurrency with async/await, actors, and task groups for safe concurrent iOS code