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

374 lines
7.0 KiB
Markdown

---
name: Modern SwiftUI
description: Modern SwiftUI API usage and best practices
globs: ["**/*.swift"]
---
# Modern SwiftUI Patterns
Use modern SwiftUI APIs and avoid deprecated patterns.
## Styling APIs
### Use foregroundStyle() Not foregroundColor()
```swift
// 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()
```swift
// 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)
```swift
// Less preferred
Text("Title")
.fontWeight(.bold)
// Preferred
Text("Title")
.bold()
```
## Navigation
### Use NavigationStack with navigationDestination
```swift
// 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
```swift
@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
```swift
// 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()
```swift
// 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()
```swift
// 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
```swift
// 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
```swift
// 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
```swift
// 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
```swift
// 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.
```swift
// 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
```swift
// Use scrollIndicators modifier
ScrollView {
// content
}
.scrollIndicators(.hidden)
```
## Button Labels with Images
### Always Include Text with Image Buttons
```swift
// 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
```swift
// 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
```swift
// 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
```swift
// For SwiftUI → Image conversion
let renderer = ImageRenderer(content: MyView())
if let uiImage = renderer.uiImage {
// use image
}
```