Copilot, Claude, Cursor, and others all read from ~/.agents/. The npx skills CLI handles fan-out to tool-specific directories.
7.0 KiB
7.0 KiB
| name | description | globs | |
|---|---|---|---|
| Modern SwiftUI | Modern SwiftUI API usage and best practices |
|
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)
}
}
Tab View
Use Tab API Not tabItem()
// BAD - Old tabItem pattern
TabView {
HomeView()
.tabItem {
Label("Home", systemImage: "house")
}
SettingsView()
.tabItem {
Label("Settings", systemImage: "gear")
}
}
// GOOD - Tab API (iOS 18+)
TabView {
Tab("Home", systemImage: "house") {
HomeView()
}
Tab("Settings", systemImage: "gear") {
SettingsView()
}
}
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
Don't Convert to Array for Enumeration
// BAD - Unnecessary Array conversion
ForEach(Array(items.enumerated()), id: \.offset) { index, item in
// ...
}
// GOOD - Use indices or zip
ForEach(items.indices, id: \.self) { index in
let item = items[index]
// ...
}
// GOOD - If you need both
ForEach(Array(zip(items.indices, items)), id: \.0) { index, item in
// ...
}
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
}