home / skills / pluginagentmarketplace / custom-plugin-swift / swift-swiftui

swift-swiftui skill

/skills/swift-swiftui

npx playbooks add skill pluginagentmarketplace/custom-plugin-swift --skill swift-swiftui

Review the files below or copy the command above to add this skill to your agents.

Files (8)
SKILL.md
8.3 KB
---
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
| Wrapper | Ownership | Use Case |
|---------|-----------|----------|
| `@State` | View owns | Local, private state |
| `@Binding` | Parent owns | Two-way child connection |
| `@StateObject` | View creates/owns | Observable object lifecycle |
| `@ObservedObject` | External owns | Passed observable |
| `@EnvironmentObject` | Environment owns | Dependency injection |
| `@Environment` | System provides | System values (colorScheme, etc) |

### Observation (iOS 17+)
| Feature | Description |
|---------|-------------|
| `@Observable` | Macro for observable classes |
| `@Bindable` | Create bindings from Observable |
| Automatic tracking | No need for @Published |

### Layout System
| Container | Purpose |
|-----------|---------|
| `VStack` | Vertical arrangement |
| `HStack` | Horizontal arrangement |
| `ZStack` | Overlapping views |
| `LazyVStack/HStack` | Lazy loading for lists |
| `Grid` | 2D grid layout |
| `GeometryReader` | Access 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

| Issue | Cause | Solution |
|-------|-------|----------|
| View not updating | Wrong property wrapper | Check ownership: @State vs @StateObject |
| Preview crash | Missing mock data | Provide preview with sample data |
| Animation stutters | Expensive body | Extract subviews, avoid complex calculations |
| Navigation broken | Missing NavigationStack | Ensure view is inside NavigationStack |
| List slow | Complex cells | Use 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

```
Skill("swift-swiftui")
```

## Related Skills

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