SwiftUI Guidelines Rules
## what it does
tl;dr: pull swiftui-guidelines-cursorrules-prompt-file.mdc from awesome-cursorrules before any SwiftUI project in Cursor. Models default to UIKit patterns, put networking in body, and miss Swift 6’s Sendable/MainActor split — every one of those is a warning-as-error in modern projects.
What it does
This rule set configures Cursor to write SwiftUI as a Senior iOS Engineer would: Swift 6.0+ with strict concurrency, MVVM with Coordinators, a 50-line body ceiling, lazy stacks with stable ids, MainActor ViewModels, actor-isolated services, and #Preview for every screen. Without it, models reach for GeometryReader, Timer, and .onAppear with a fetched Task that nobody cancels.
Sourced from PatrickJS/awesome-cursorrules — the community registry for .mdc Project Rules.
The rules
# Role & Perspective
You are a Senior iOS Engineer and SwiftUI Expert. You are also an expert in Clean Architecture, SOLID Principles, Design Patterns (MVVM-C, VIPER), and Performance Optimization.
# Code Generation Guidelines
## 1. General Principles
- Language: Swift 6.0+ (Strict Concurrency).
- Framework: SwiftUI (Targeting iOS 15+).
- Architecture: MVVM or Clean Architecture. Use Coordinators for complex navigation.
- Design Principles (SOLID): SRP, OCP, LSP, ISP, DIP.
- Safety: STRICTLY enforce Swift 6 concurrency safety (Sendable, MainActor, actor). Treat warnings as errors.
## 2. SwiftUI Best Practices (Performance First)
- View Composition:
- Strict Size Limit: body MUST NOT exceed 50 lines. Relentlessly extract subviews.
- GeometryReader: Avoid unless absolutely necessary. Prefer .background(GeometryReader ...) or Layout protocol.
- Modifiers:
- Hit Testing: Add .contentShape(Rectangle()) to rows with transparent backgrounds.
- Shorthand Syntax: Prefer type-inferred dot syntax (e.g., .background(.blue)).
- State Management:
- @State: local value-type properties.
- @StateObject: ONLY in the view that creates/owns the lifecycle.
- @ObservedObject: child views that react but do not own.
- @EnvironmentObject: sparingly. Prefer explicit DI via init.
- Lists & Grids:
- LazyVStack / LazyHStack for dynamic content.
- Always use stable id (avoid \.self).
- Animation:
- Use .animation(_:value:) explicitly linked to a state variable.
- Avoid withAnimation inside body for state changes unless triggered by user interaction.
- TimelineView for high-frequency updates instead of Timer.
## 3. Swift 6 Concurrency & Threading
- Main Thread: UI updates MUST run on the MainActor.
- Annotate ViewModels with @MainActor.
- Use MainActor.run { ... } or Task { @MainActor in ... } for context switching.
- Lifecycle: Prefer .task(id: ...) over .onAppear (auto-cancels async work).
- Data Layer: actor for shared mutable state/services. nonisolated for pure logic.
- Blocking: NEVER block the main thread. Heavy work goes to a detached Task.
## 4. Memory Management & Safety
- Closures: Default to [weak self]. Strictly use guard let self else { return } at the start of async closures. ONLY use [unowned self] with proven lifecycle.
- Image Handling: AsyncImage (with caching) or Nuke/Kingfisher. Always apply .resizable() immediately.
## 5. Coding Style & Naming
- Naming: Verbose and clear. fetchUserData > getData.
- Structure: MARK: - Section Name. Private helpers in private extension.
- Previews: Always provide #Preview (Xcode 15+) or PreviewProvider. Inject Mock data.
## 6. Testing Strategy
- Unit Tests: Given-When-Then pattern.
- Mocks: Protocol-based Mocks for external dependencies.
- UITests: distinct accessibilityIdentifier strings.
# Response Format
- Block-based: Return code in formatted code blocks.
- Reasoning: Briefly explain why a specific approach was chosen.
- Diffs: Prioritize showing specific changes or full corrected context.
Installation
Drop the .mdc into .cursor/rules/ of your SwiftUI project:
mkdir -p .cursor/rules
curl -sL \
https://raw.githubusercontent.com/PatrickJS/awesome-cursorrules/main/rules/swiftui-guidelines-cursorrules-prompt-file.mdc \
-o .cursor/rules/swiftui-guidelines.mdc
Cursor picks up files in .cursor/rules/ automatically.
Example View + ViewModel
@MainActor
final class UserListViewModel: ObservableObject {
@Published private(set) var users: [User] = []
@Published private(set) var isLoading = false
private let service: UserService
init(service: UserService = .live) {
self.service = service
}
func load() async {
isLoading = true
defer { isLoading = false }
do {
users = try await service.fetchUsers()
} catch {
// typed domain error, not generic Exception
}
}
}
struct UserListView: View {
@StateObject private var viewModel = UserListViewModel()
var body: some View {
// keep this under ~50 lines — extract subviews aggressively
NavigationStack {
List(viewModel.users) { user in
UserRow(user: user)
.contentShape(Rectangle())
}
.task { await viewModel.load() }
.navigationTitle("Users")
}
}
}
Why it helps
Without SwiftUI-aware rules, models will:
- Stuff a 200-line
bodyinto one struct (every redraw re-evaluates the whole tree) - Use
GeometryReaderinstead ofLayoutprotocol or.background(GeometryReader ...) - Pass
Timerpublishers whereTimelineViewis the right tool - Reach for
\.selfas a listid(crashes on duplicate IDs, and breaks animations) - Skip
@MainActorannotations on ViewModels and get “publishing changes from background threads” warnings - Use
.onAppear { Task { await load() } }and never cancel — leaks when the view pops mid-flight - Forget
[weak self]in async closures, leaking the ViewModel
These rules turn those mistakes into compile errors before they ship.
Gotchas
- The body 50-line limit is enforced by the rule, not the compiler. Pair with an Xcode file-template or SwiftLint custom rule if your team needs the teeth.
alwaysApply: falsein the frontmatter — these are Project Rules you opt into per repo, not a global hook.- These are Cursor
.mdcrules, not a Claude CodeCLAUDE.mdfile. For Claude Code, paraphrase into your project’sCLAUDE.md. - “Strict concurrency” assumes Swift 6 mode. On Swift 5.x toolchains you’ll see a flood of warnings — fix the warnings or upgrade.
#Previewrequires Xcode 15+. On older Xcode the rule wantsPreviewProviderinstead.
Related
- React Router v7 Rules — same route-as-boundary idea for SwiftUI’s
NavigationStack/NavigationPath - TanStack Query v5 Rules — analogous server-state separation; pairs cleanly with an
@MainActorViewModel
## embed this badge
