swiftui-patterns

SwiftUI development patterns for view composition, state management, performance optimization, and UI best practices.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "swiftui-patterns" with this command: npx skills add sh-oh/ios-agent-skills/sh-oh-ios-agent-skills-swiftui-patterns

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

PatternWhen to UseReference
Card / Compound ComponentsReusable UI building blocksview-composition.md
ViewBuilder ClosuresGeneric containers with custom contentview-composition.md
Custom ViewModifierReusable styling and behaviorview-composition.md
PreferenceKeyReading child geometry (size, offset)view-composition.md
Custom Layout (iOS 16+)FlowLayout, tag cloudsview-composition.md
@Observable + ReducerPredictable state with actionsstate-management.md
@Observable / @BindableSimple iOS 17+ state managementstate-management.md
Environment DITestable dependency injectionstate-management.md
TCAComposable Architecture appsstate-management.md
NavigationStack + RouteType-safe navigation (iOS 16+)state-management.md
LoadingState enumIdle/loading/loaded/failed statesstate-management.md
PaginationInfinite scroll with prefetchstate-management.md
Implicit / Explicit AnimationTransitions and spring animationsanimation.md
Matched Geometry EffectHero transitions between viewsanimation.md
Reduce MotionAccessibility-safe animationsanimation.md
#Preview Smart GenerationAuto-embedding rules for previewspreview-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.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

ios-code-review

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

xcode-build-resolver

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

liquid-glass

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

swift-coding-standards

No summary provided by upstream source.

Repository SourceNeeds Review