Files
gh-glittercowboy-taches-cc-…/skills/expertise/iphone-apps/references/swiftui-patterns.md
2025-11-29 18:28:37 +08:00

11 KiB

SwiftUI Patterns

Modern SwiftUI patterns for iOS 26 with iOS 18 compatibility.

View Composition

Small, Focused Views

// Bad: Massive view
struct ContentView: View {
    var body: some View {
        VStack {
            // 200 lines of UI code
        }
    }
}

// Good: Composed from smaller views
struct ContentView: View {
    var body: some View {
        VStack {
            HeaderView()
            ItemList()
            ActionBar()
        }
    }
}

struct HeaderView: View {
    var body: some View {
        // Focused implementation
    }
}

Extract Subviews

struct ItemRow: View {
    let item: Item

    var body: some View {
        HStack {
            iconView
            contentView
            Spacer()
            chevronView
        }
    }

    private var iconView: some View {
        Image(systemName: item.icon)
            .foregroundStyle(.accent)
            .frame(width: 30)
    }

    private var contentView: some View {
        VStack(alignment: .leading) {
            Text(item.name)
                .font(.headline)
            Text(item.subtitle)
                .font(.caption)
                .foregroundStyle(.secondary)
        }
    }

    private var chevronView: some View {
        Image(systemName: "chevron.right")
            .foregroundStyle(.tertiary)
            .font(.caption)
    }
}

Async Data Loading

Task Modifier

struct ItemList: View {
    @State private var items: [Item] = []
    @State private var isLoading = true
    @State private var error: Error?

    var body: some View {
        Group {
            if isLoading {
                ProgressView()
            } else if let error {
                ErrorView(error: error, retry: load)
            } else {
                List(items) { item in
                    ItemRow(item: item)
                }
            }
        }
        .task {
            await load()
        }
    }

    private func load() async {
        isLoading = true
        defer { isLoading = false }

        do {
            items = try await fetchItems()
        } catch {
            self.error = error
        }
    }
}

Refresh Control

struct ItemList: View {
    @State private var items: [Item] = []

    var body: some View {
        List(items) { item in
            ItemRow(item: item)
        }
        .refreshable {
            items = try? await fetchItems()
        }
    }
}

Task with ID

Reload when identifier changes:

struct ItemDetail: View {
    let itemID: UUID
    @State private var item: Item?

    var body: some View {
        Group {
            if let item {
                ItemContent(item: item)
            } else {
                ProgressView()
            }
        }
        .task(id: itemID) {
            item = try? await fetchItem(id: itemID)
        }
    }
}

Lists and Grids

Swipe Actions

List {
    ForEach(items) { item in
        ItemRow(item: item)
            .swipeActions(edge: .trailing) {
                Button(role: .destructive) {
                    delete(item)
                } label: {
                    Label("Delete", systemImage: "trash")
                }

                Button {
                    archive(item)
                } label: {
                    Label("Archive", systemImage: "archivebox")
                }
                .tint(.orange)
            }
            .swipeActions(edge: .leading) {
                Button {
                    toggleFavorite(item)
                } label: {
                    Label("Favorite", systemImage: item.isFavorite ? "star.fill" : "star")
                }
                .tint(.yellow)
            }
    }
}

Lazy Grids

struct PhotoGrid: View {
    let photos: [Photo]
    let columns = [GridItem(.adaptive(minimum: 100), spacing: 2)]

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 2) {
                ForEach(photos) { photo in
                    AsyncImage(url: photo.thumbnailURL) { image in
                        image
                            .resizable()
                            .aspectRatio(1, contentMode: .fill)
                    } placeholder: {
                        Color.gray.opacity(0.3)
                    }
                    .clipped()
                }
            }
        }
    }
}

Sections with Headers

List {
    ForEach(groupedItems, id: \.key) { section in
        Section(section.key) {
            ForEach(section.items) { item in
                ItemRow(item: item)
            }
        }
    }
}
.listStyle(.insetGrouped)

Forms and Input

Form with Validation

struct ProfileForm: View {
    @State private var name = ""
    @State private var email = ""
    @State private var bio = ""

    private var isValid: Bool {
        !name.isEmpty && email.contains("@") && email.contains(".")
    }

    var body: some View {
        Form {
            Section("Personal Info") {
                TextField("Name", text: $name)
                    .textContentType(.name)

                TextField("Email", text: $email)
                    .textContentType(.emailAddress)
                    .keyboardType(.emailAddress)
                    .autocapitalization(.none)
            }

            Section("About") {
                TextField("Bio", text: $bio, axis: .vertical)
                    .lineLimit(3...6)
            }

            Section {
                Button("Save") {
                    save()
                }
                .disabled(!isValid)
            }
        }
    }
}

Pickers

struct SettingsView: View {
    @State private var selectedTheme = Theme.system
    @State private var fontSize = 16.0

    var body: some View {
        Form {
            Picker("Theme", selection: $selectedTheme) {
                ForEach(Theme.allCases) { theme in
                    Text(theme.rawValue).tag(theme)
                }
            }

            Section("Text Size") {
                Slider(value: $fontSize, in: 12...24, step: 1) {
                    Text("Font Size")
                } minimumValueLabel: {
                    Text("A").font(.caption)
                } maximumValueLabel: {
                    Text("A").font(.title)
                }
                .padding(.vertical)
            }
        }
    }
}

Sheets and Alerts

Sheet Presentation

struct ContentView: View {
    @State private var showingSettings = false
    @State private var selectedItem: Item?

    var body: some View {
        List(items) { item in
            Button(item.name) {
                selectedItem = item
            }
        }
        .toolbar {
            Button {
                showingSettings = true
            } label: {
                Image(systemName: "gear")
            }
        }
        .sheet(isPresented: $showingSettings) {
            SettingsView()
        }
        .sheet(item: $selectedItem) { item in
            ItemDetail(item: item)
        }
    }
}

Confirmation Dialogs

struct ItemRow: View {
    let item: Item
    @State private var showingDeleteConfirmation = false

    var body: some View {
        HStack {
            Text(item.name)
            Spacer()
            Button(role: .destructive) {
                showingDeleteConfirmation = true
            } label: {
                Image(systemName: "trash")
            }
        }
        .confirmationDialog(
            "Delete \(item.name)?",
            isPresented: $showingDeleteConfirmation,
            titleVisibility: .visible
        ) {
            Button("Delete", role: .destructive) {
                delete(item)
            }
        } message: {
            Text("This action cannot be undone.")
        }
    }
}

iOS 26 Features

Liquid Glass

struct GlassCard: View {
    var body: some View {
        VStack {
            Text("Premium Content")
                .font(.headline)
        }
        .padding()
        .background(.regularMaterial)
        .clipShape(RoundedRectangle(cornerRadius: 16))
        // iOS 26 glass effect
        .glassEffect()
    }
}

// Availability check
struct AdaptiveCard: View {
    var body: some View {
        if #available(iOS 26, *) {
            GlassCard()
        } else {
            StandardCard()
        }
    }
}

WebView

import WebKit

// iOS 26+ native WebView
struct WebContent: View {
    let url: URL

    var body: some View {
        if #available(iOS 26, *) {
            WebView(url: url)
                .ignoresSafeArea()
        } else {
            WebViewRepresentable(url: url)
        }
    }
}

// Fallback for iOS 18
struct WebViewRepresentable: UIViewRepresentable {
    let url: URL

    func makeUIView(context: Context) -> WKWebView {
        WKWebView()
    }

    func updateUIView(_ webView: WKWebView, context: Context) {
        webView.load(URLRequest(url: url))
    }
}

@Animatable Macro

// iOS 26+
@available(iOS 26, *)
@Animatable
struct PulsingCircle: View {
    var scale: Double

    var body: some View {
        Circle()
            .scaleEffect(scale)
    }
}

Custom Modifiers

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())
    }
}

// Usage
Text("Content")
    .cardStyle()

Conditional Modifiers

extension View {
    @ViewBuilder
    func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
        if condition {
            transform(self)
        } else {
            self
        }
    }
}

// Usage
Text("Item")
    .if(isHighlighted) { view in
        view.foregroundStyle(.accent)
    }

Preview Techniques

Multiple Configurations

#Preview("Light Mode") {
    ItemRow(item: .sample)
        .preferredColorScheme(.light)
}

#Preview("Dark Mode") {
    ItemRow(item: .sample)
        .preferredColorScheme(.dark)
}

#Preview("Large Text") {
    ItemRow(item: .sample)
        .environment(\.sizeCategory, .accessibilityExtraLarge)
}

Interactive Previews

#Preview {
    @Previewable @State var isOn = false

    Toggle("Setting", isOn: $isOn)
        .padding()
}

Preview with Mock Data

extension Item {
    static let sample = Item(
        name: "Sample Item",
        subtitle: "Sample subtitle",
        icon: "star"
    )

    static let samples: [Item] = [
        Item(name: "First", subtitle: "One", icon: "1.circle"),
        Item(name: "Second", subtitle: "Two", icon: "2.circle"),
        Item(name: "Third", subtitle: "Three", icon: "3.circle")
    ]
}

#Preview {
    List(Item.samples) { item in
        ItemRow(item: item)
    }
}