skills/sh-oh/ios-agent-skills/swiftui-patterns

swiftui-patterns

SKILL.md

SwiftUI Development Patterns

Modern SwiftUI patterns for building performant, maintainable user interfaces.

When to Apply

  • Building new SwiftUI views or refactoring existing ones
  • Choosing state management strategy (@State, @Observable, TCA)
  • Optimizing list/scroll performance (lazy containers, equatable views)
  • Implementing forms with validation
  • Adding animations and transitions
  • Setting up navigation (NavigationStack, coordinator)
  • Writing #Preview blocks for any SwiftUI view

Quick Reference

Pattern When to Use Reference
Card / Compound Components Reusable UI building blocks view-composition.md
ViewBuilder Closures Generic containers with custom content view-composition.md
Custom ViewModifier Reusable styling and behavior view-composition.md
PreferenceKey Reading child geometry (size, offset) view-composition.md
Custom Layout (iOS 16+) FlowLayout, tag clouds view-composition.md
@Observable + Reducer Predictable state with actions state-management.md
@Observable / @Bindable Simple iOS 17+ state management state-management.md
Environment DI Testable dependency injection state-management.md
TCA Composable Architecture apps state-management.md
NavigationStack + Route Type-safe navigation (iOS 16+) state-management.md
LoadingState enum Idle/loading/loaded/failed states state-management.md
Pagination Infinite scroll with prefetch state-management.md
Implicit / Explicit Animation Transitions and spring animations animation.md
Matched Geometry Effect Hero transitions between views animation.md
Reduce Motion Accessibility-safe animations animation.md
#Preview Smart Generation Auto-embedding rules for previews preview-rules.md

Key Patterns

View Composition

// Card pattern with @ViewBuilder
struct Card<Content: View>: View {
    let content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            content
        }
        .background(Color(.systemBackground))
        .cornerRadius(12)
        .shadow(radius: 2)
    }
}

State Management Overview

// @State: Local value state
@State private var count = 0

// @Binding: Two-way reference to parent state
@Binding var isPresented: Bool

// @Observable (iOS 17+): Reference type state
@Observable
final class AppState {
    var user: User?
    var isAuthenticated: Bool { user != nil }
}

// Usage in views
struct ContentView: View {
    @State private var appState = AppState()

    var body: some View {
        MainView()
            .environment(appState)
    }
}

struct ProfileView: View {
    @Environment(AppState.self) private var appState

    var body: some View {
        if let user = appState.user {
            Text(user.name)
        }
    }
}

Performance: Lazy Containers

// GOOD: Lazy loading for long lists
ScrollView {
    LazyVStack(spacing: 12) {
        ForEach(items) { item in
            ItemCard(item: item)
        }
    }
    .padding()
}

// BAD: Regular VStack loads all views immediately
ScrollView {
    VStack {  // All 1000 views rendered at once
        ForEach(items) { item in
            ItemCard(item: item)
        }
    }
}

Performance: Equatable Views

struct ItemCard: View, Equatable {
    let item: Item

    static func == (lhs: ItemCard, rhs: ItemCard) -> Bool {
        lhs.item.id == rhs.item.id &&
        lhs.item.name == rhs.item.name
    }

    var body: some View {
        VStack {
            Text(item.name)
            StatusBadge(status: item.status)
        }
    }
}

// Usage
ForEach(items) { item in
    EquatableView(content: ItemCard(item: item))
}

Performance: Computed Properties vs State

// GOOD: Computed properties for derived data
var filteredItems: [Item] {
    guard !searchQuery.isEmpty else { return items }
    return items.filter { $0.name.localizedCaseInsensitiveContains(searchQuery) }
}

// BAD: Storing derived state (out-of-sync risk)
@State private var filteredItems: [Item] = []  // Redundant state

Form Handling

struct CreateItemForm: View {
    @State private var name = ""
    @State private var description = ""
    @State private var errors: [FieldError] = []

    enum FieldError: Identifiable {
        case nameTooShort, nameTooLong, descriptionEmpty

        var id: String { String(describing: self) }
        var message: String {
            switch self {
            case .nameTooShort: "Name is required"
            case .nameTooLong: "Name must be under 200 characters"
            case .descriptionEmpty: "Description is required"
            }
        }
    }

    private var isValid: Bool {
        errors.isEmpty && !name.isEmpty && !description.isEmpty
    }

    var body: some View {
        Form {
            Section("Details") {
                TextField("Name", text: $name)
                if let error = errors.first(where: { $0 == .nameTooShort || $0 == .nameTooLong }) {
                    Text(error.message)
                        .foregroundColor(.red)
                        .font(.caption)
                }
                TextField("Description", text: $description, axis: .vertical)
                    .lineLimit(3...6)
            }
            Button("Submit") { submit() }
                .disabled(!isValid)
        }
        .onChange(of: name) { validate() }
        .onChange(of: description) { validate() }
    }

    private func validate() { /* validate fields, populate errors */ }
    private func submit() {
        validate()
        guard isValid else { return }
    }
}

Animation Overview

// Implicit animation
List(items) { item in
    ItemRow(item: item)
        .transition(.asymmetric(
            insertion: .opacity.combined(with: .move(edge: .trailing)),
            removal: .opacity.combined(with: .move(edge: .leading))
        ))
}
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: items)

// Explicit animation
withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) {
    isExpanded.toggle()
}

References

Common Mistakes

1. Using VStack instead of LazyVStack in ScrollView

Regular VStack inside ScrollView instantiates all child views immediately. Always use LazyVStack for lists with more than ~20 items.

2. Storing Derived State

Do not store filtered/sorted/mapped versions of existing state in separate @State properties. Use computed properties instead to avoid synchronization bugs.

3. Using Legacy PreviewProvider

// NEVER use PreviewProvider (legacy)
struct MyView_Previews: PreviewProvider {
    static var previews: some View { ... }
}

// ALWAYS use #Preview macro
#Preview { MyView() }

4. Missing weak self in Combine/Timer Closures

Always use [weak self] in escaping closures (Combine sinks, Timer callbacks) to prevent retain cycles. The .task modifier handles cancellation automatically.

5. Overusing @StateObject / @ObservedObject

On iOS 17+, prefer @Observable with @State (for ownership) or @Environment (for injection) instead of ObservableObject + @StateObject / @ObservedObject.


Remember: Choose patterns that fit your project complexity. Start simple and add abstractions only when needed.

Weekly Installs
4
First Seen
4 days ago
Installed on
amp4
cline4
opencode4
cursor4
kimi-cli4
codex4