AgentSkillsCN

swift-swiftui

使用 SwiftUI 构建现代 UI——视图、状态管理、动画、导航

SKILL.md
--- frontmatter
name: swift-swiftui
description: Build modern UIs with SwiftUI - views, state management, animations, navigation
version: "2.0.0"
sasmp_version: "1.3.0"
bonded_agent: 03-swift-swiftui
bond_type: PRIMARY_BOND

SwiftUI Skill

Declarative UI framework knowledge for building modern Apple platform interfaces.

Prerequisites

  • Xcode 15+ installed
  • iOS 16+ / macOS 13+ deployment target recommended
  • Understanding of reactive programming concepts

Parameters

yaml
parameters:
  min_ios_version:
    type: string
    default: "16.0"
    description: Minimum iOS version
  platforms:
    type: array
    items: [iOS, macOS, watchOS, tvOS, visionOS]
    default: [iOS]
  observation_framework:
    type: string
    enum: [observation, combine, observable_object]
    default: observation
    description: State management approach

Topics Covered

Property Wrappers

WrapperOwnershipUse Case
@StateView ownsLocal, private state
@BindingParent ownsTwo-way child connection
@StateObjectView creates/ownsObservable object lifecycle
@ObservedObjectExternal ownsPassed observable
@EnvironmentObjectEnvironment ownsDependency injection
@EnvironmentSystem providesSystem values (colorScheme, etc)

Observation (iOS 17+)

FeatureDescription
@ObservableMacro for observable classes
@BindableCreate bindings from Observable
Automatic trackingNo need for @Published

Layout System

ContainerPurpose
VStackVertical arrangement
HStackHorizontal arrangement
ZStackOverlapping views
LazyVStack/HStackLazy loading for lists
Grid2D grid layout
GeometryReaderAccess to size/position

Code Examples

Observation Pattern (iOS 17+)

swift
import SwiftUI

@Observable
final class ShoppingCart {
    var items: [CartItem] = []
    var couponCode: String = ""

    var subtotal: Decimal {
        items.reduce(0) { $0 + $1.price * Decimal($1.quantity) }
    }

    var total: Decimal {
        let discount = applyCoupon(to: subtotal)
        return subtotal - discount
    }

    func add(_ product: Product, quantity: Int = 1) {
        if let index = items.firstIndex(where: { $0.product.id == product.id }) {
            items[index].quantity += quantity
        } else {
            items.append(CartItem(product: product, quantity: quantity))
        }
    }

    func remove(_ item: CartItem) {
        items.removeAll { $0.id == item.id }
    }

    private func applyCoupon(to amount: Decimal) -> Decimal {
        guard !couponCode.isEmpty else { return 0 }
        // Apply coupon logic
        return amount * 0.1
    }
}

struct CartView: View {
    @Bindable var cart: ShoppingCart

    var body: some View {
        List {
            ForEach(cart.items) { item in
                CartItemRow(item: item)
            }
            .onDelete { indexSet in
                cart.items.remove(atOffsets: indexSet)
            }

            Section {
                HStack {
                    TextField("Coupon code", text: $cart.couponCode)
                    Button("Apply") { }
                }

                LabeledContent("Subtotal", value: cart.subtotal, format: .currency(code: "USD"))
                LabeledContent("Total", value: cart.total, format: .currency(code: "USD"))
                    .fontWeight(.bold)
            }
        }
        .navigationTitle("Cart (\(cart.items.count))")
    }
}

Custom View Modifier

swift
struct CardStyle: ViewModifier {
    let cornerRadius: CGFloat
    let shadowRadius: CGFloat

    func body(content: Content) -> some View {
        content
            .background(.background)
            .clipShape(RoundedRectangle(cornerRadius: cornerRadius))
            .shadow(color: .black.opacity(0.1), radius: shadowRadius, y: 2)
    }
}

extension View {
    func cardStyle(cornerRadius: CGFloat = 12, shadowRadius: CGFloat = 4) -> some View {
        modifier(CardStyle(cornerRadius: cornerRadius, shadowRadius: shadowRadius))
    }
}

// Usage
struct ProductCard: View {
    let product: Product

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            AsyncImage(url: product.imageURL) { image in
                image.resizable().aspectRatio(contentMode: .fill)
            } placeholder: {
                ProgressView()
            }
            .frame(height: 150)
            .clipped()

            Text(product.name)
                .font(.headline)

            Text(product.price, format: .currency(code: "USD"))
                .foregroundStyle(.secondary)
        }
        .cardStyle()
    }
}

Custom Animations

swift
struct PulsingButton: View {
    let title: String
    let action: () -> Void

    @State private var isPulsing = false

    var body: some View {
        Button(action: action) {
            Text(title)
                .font(.headline)
                .foregroundStyle(.white)
                .padding(.horizontal, 24)
                .padding(.vertical, 12)
                .background(.blue)
                .clipShape(Capsule())
                .scaleEffect(isPulsing ? 1.05 : 1.0)
        }
        .onAppear {
            withAnimation(.easeInOut(duration: 0.8).repeatForever(autoreverses: true)) {
                isPulsing = true
            }
        }
    }
}

struct MatchedGeometryExample: View {
    @Namespace private var animation
    @State private var isExpanded = false

    var body: some View {
        VStack {
            if isExpanded {
                RoundedRectangle(cornerRadius: 20)
                    .fill(.blue)
                    .matchedGeometryEffect(id: "shape", in: animation)
                    .frame(height: 300)
            } else {
                RoundedRectangle(cornerRadius: 10)
                    .fill(.blue)
                    .matchedGeometryEffect(id: "shape", in: animation)
                    .frame(width: 100, height: 100)
            }
        }
        .onTapGesture {
            withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) {
                isExpanded.toggle()
            }
        }
    }
}

Navigation Stack (iOS 16+)

swift
struct NavigationExample: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            List(products) { product in
                NavigationLink(value: product) {
                    ProductRow(product: product)
                }
            }
            .navigationTitle("Products")
            .navigationDestination(for: Product.self) { product in
                ProductDetailView(product: product)
            }
            .navigationDestination(for: Category.self) { category in
                CategoryView(category: category)
            }
        }
    }

    func navigateToProduct(_ product: Product) {
        path.append(product)
    }

    func popToRoot() {
        path.removeLast(path.count)
    }
}

Troubleshooting

Common Issues

IssueCauseSolution
View not updatingWrong property wrapperCheck ownership: @State vs @StateObject
Preview crashMissing mock dataProvide preview with sample data
Animation stuttersExpensive bodyExtract subviews, avoid complex calculations
Navigation brokenMissing NavigationStackEnsure view is inside NavigationStack
List slowComplex cellsUse LazyVStack, simplify cell views

Debug Tips

swift
// Trace view updates
var body: some View {
    let _ = Self._printChanges()
    // ... view content
}

// Check if preview
#if DEBUG
struct MyView_Previews: PreviewProvider {
    static var previews: some View {
        MyView(data: .preview)
    }
}
#endif

Validation Rules

yaml
validation:
  - rule: state_ownership
    severity: error
    check: @StateObject for views that create, @ObservedObject for passed
  - rule: body_purity
    severity: warning
    check: No side effects in body computed property
  - rule: lazy_for_lists
    severity: info
    check: Use LazyVStack/LazyHStack for long scrolling content

Usage

code
Skill("swift-swiftui")

Related Skills

  • swift-combine - Reactive programming
  • swift-uikit - UIKit interop
  • swift-architecture - MVVM patterns