ai-docs/assets/skills/swiftui-modern/SKILL.md

7.0 KiB

name description globs
Modern SwiftUI Modern SwiftUI API usage and best practices
**/*.swift

Modern SwiftUI Patterns

Use modern SwiftUI APIs and avoid deprecated patterns.

Styling APIs

Use foregroundStyle() Not foregroundColor()

// BAD - Deprecated
Text("Hello")
    .foregroundColor(.blue)

// GOOD
Text("Hello")
    .foregroundStyle(.blue)

// GOOD - With gradients
Text("Hello")
    .foregroundStyle(.linearGradient(colors: [.blue, .purple], startPoint: .leading, endPoint: .trailing))

Use clipShape(.rect()) Not cornerRadius()

// BAD - Deprecated
Image("photo")
    .cornerRadius(12)

// GOOD
Image("photo")
    .clipShape(.rect(cornerRadius: 12))

// GOOD - With specific corners
Image("photo")
    .clipShape(.rect(cornerRadii: .init(topLeading: 12, topTrailing: 12)))

Use bold() Not fontWeight(.bold)

// Less preferred
Text("Title")
    .fontWeight(.bold)

// Preferred
Text("Title")
    .bold()

Navigation

Use NavigationStack with navigationDestination

// BAD - Old NavigationView with NavigationLink
NavigationView {
    List(items) { item in
        NavigationLink(destination: DetailView(item: item)) {
            ItemRow(item: item)
        }
    }
}

// GOOD - NavigationStack with typed destinations
NavigationStack {
    List(items) { item in
        NavigationLink(value: item) {
            ItemRow(item: item)
        }
    }
    .navigationDestination(for: Item.self) { item in
        DetailView(item: item)
    }
}

Use NavigationPath for Programmatic Navigation

@Observable
@MainActor
final class NavigationStore {
    var path = NavigationPath()
    
    func navigate(to item: Item) {
        path.append(item)
    }
    
    func popToRoot() {
        path.removeLast(path.count)
    }
}

Observable Pattern

Use @Observable Not ObservableObject

// BAD - Old Combine-based pattern
class FeatureStore: ObservableObject {
    @Published var items: [Item] = []
}

struct FeatureView: View {
    @StateObject var store = FeatureStore()
    // or @ObservedObject
}

// GOOD - Modern Observation
@Observable
@MainActor
final class FeatureStore {
    var items: [Item] = []
}

struct FeatureView: View {
    @State var store = FeatureStore()
    // or for external injection:
    @Bindable var store: FeatureStore
}

Event Handling

Use Button Not onTapGesture()

// BAD - No accessibility, no button styling
Text("Submit")
    .onTapGesture {
        submit()
    }

// GOOD - Proper button semantics
Button("Submit") {
    submit()
}

// When you need tap location/count, onTapGesture is acceptable
SomeView()
    .onTapGesture(count: 2) { location in
        handleDoubleTap(at: location)
    }

Use Two-Parameter onChange()

// BAD - Deprecated single parameter
.onChange(of: searchText) { newValue in
    search(for: newValue)
}

// GOOD - Two parameter version
.onChange(of: searchText) { oldValue, newValue in
    search(for: newValue)
}

// GOOD - When you don't need old value
.onChange(of: searchText) { _, newValue in
    search(for: newValue)
}

Layout

Avoid UIScreen.main.bounds

// BAD - Hardcoded screen size
let width = UIScreen.main.bounds.width

// GOOD - GeometryReader when needed
GeometryReader { geometry in
    SomeView()
        .frame(width: geometry.size.width * 0.8)
}

// BETTER - containerRelativeFrame (iOS 17+)
SomeView()
    .containerRelativeFrame(.horizontal) { size, _ in
        size * 0.8
    }

Prefer containerRelativeFrame Over GeometryReader

// Avoid GeometryReader when possible
ScrollView(.horizontal) {
    LazyHStack {
        ForEach(items) { item in
            ItemCard(item: item)
                .containerRelativeFrame(.horizontal, count: 3, spacing: 16)
        }
    }
}

View Composition

Extract to View Structs Not Computed Properties

// BAD - Computed properties for view composition
struct ContentView: View {
    private var header: some View {
        HStack {
            Text("Title")
            Spacer()
            Button("Action") { }
        }
    }
    
    var body: some View {
        VStack {
            header
            // ...
        }
    }
}

// GOOD - Separate View struct
struct HeaderView: View {
    let title: String
    let action: () -> Void
    
    var body: some View {
        HStack {
            Text(title)
            Spacer()
            Button("Action", action: action)
        }
    }
}

Avoid AnyView

// BAD - Type erasure loses optimization
func makeView(for type: ViewType) -> AnyView {
    switch type {
    case .list: return AnyView(ListView())
    case .grid: return AnyView(GridView())
    }
}

// GOOD - @ViewBuilder
@ViewBuilder
func makeView(for type: ViewType) -> some View {
    switch type {
    case .list: ListView()
    case .grid: GridView()
    }
}

Lists and ForEach

Use Identifiable Conformance for ForEach

Let ForEach use Identifiable conformance directly — don't bypass it with manual key paths or offset-based identity.

// BAD - offset-based id breaks animations when items change
ForEach(Array(items.enumerated()), id: \.offset) { index, item in
    // ...
}

// BAD - index-based id has the same animation problem
ForEach(items.indices, id: \.self) { index in
    let item = items[index]
    // ...
}

// GOOD - Identifiable items work directly (protocol-based)
ForEach(items) { item in
    // ...
}

// GOOD - When index is also needed, use enumerated keyed on the item's identity
ForEach(Array(items.enumerated()), id: \.element.id) { index, item in
    // Stable identity comes from item.id (Identifiable conformance),
    // not from the offset — so animations and diffing work correctly.
}

Hide Scroll Indicators

// Use scrollIndicators modifier
ScrollView {
    // content
}
.scrollIndicators(.hidden)

Button Labels with Images

Always Include Text with Image Buttons

// BAD - No accessibility label
Button {
    addItem()
} label: {
    Image(systemName: "plus")
}

// GOOD - Text alongside image
Button {
    addItem()
} label: {
    Label("Add Item", systemImage: "plus")
}

// GOOD - If you only want to show the image
Button {
    addItem()
} label: {
    Label("Add Item", systemImage: "plus")
        .labelStyle(.iconOnly)
}

Design Constants

Never Use Raw Numeric Literals

// BAD
.padding(16)
.clipShape(.rect(cornerRadius: 12))
.opacity(0.7)

// GOOD - Use design constants
.padding(Design.Spacing.medium)
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
.opacity(Design.Opacity.strong)

Never Use Inline Colors

// BAD
.foregroundStyle(Color(red: 0.2, green: 0.4, blue: 0.8))
.background(Color(hex: "#3366CC"))

// GOOD - Semantic color names
.foregroundStyle(Color.Theme.primary)
.background(Color.Background.secondary)

Image Rendering

Prefer ImageRenderer Over UIGraphicsImageRenderer

// For SwiftUI → Image conversion
let renderer = ImageRenderer(content: MyView())
if let uiImage = renderer.uiImage {
    // use image
}