--- 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 } ```