Promptheus/rules53 rule sets · CC0Promptheus hub ↗

Mobile · Swift 6.3 · SwiftUI · iOS 26 · Xcode 26

SwiftUI

Value types, @Observable and async/await — modern iOS.

swiftswiftuiiosmobile

Updated 5 Jul 2026 · CC0

AGENTS.mdrepo root

You are a staff iOS engineer writing SwiftUI. "Good" here means small composable value-driven views, all state in @Observable models, strict Swift 6 concurrency with zero data races, typed navigation, and no deprecated pre-Observation idioms. Ship code that compiles clean under the Swift 6 language mode with warnings-as-errors.

Stack

  • Swift 6.3 (6.3.x), Swift 6 language mode with complete strict concurrency and data-race safety on. Not a suggestion — set it in every target.
  • Xcode 26.x (26.6 stable), iOS 26 SDK. Build against iOS 26; minimum deployment iOS 18 for greenfield unless product says otherwise. (Xcode 27 / Swift 6.4 / iOS 27 are WWDC26 betas — do not pin them for shipping code.)
  • SwiftUI as the only UI layer; drop to UIKit/UIViewRepresentable only for gaps (rich text editing, PHPickerViewController, camera).
  • Observation framework (@Observable, @State, @Bindable) — replaces ObservableObject/@Published entirely on iOS 17+.
  • Structured concurrency: async/await, Task, actor, @MainActor, Sendable, AsyncStream/AsyncSequence. No Combine for new async work.
  • SwiftData (@Model, @Query, ModelContainer) for local persistence — not Core Data for new code.
  • Swift Testing (import Testing, @Test, #expect) as the default test framework; XCTest only for legacy and XCUITest UI flows.
  • Swift Package Manager for all dependencies and internal module boundaries. No CocoaPods, no Carthage.
  • swift-format (bundled: swift format) as the formatter; SwiftLint for lint rules CI enforces.
  • iOS 26 UI material: Liquid Glass via .glassEffect(_:in:), GlassEffectContainer, .buttonStyle(.glass) — gate behind if #available(iOS 26, *).

Project conventions

  • Modularize with SPM: one local package per feature (Features/Profile, Features/Feed), plus Core, DesignSystem, Networking packages. Keep the app target thin — wiring only.
  • Default access control is internal; mark module public API public deliberately. Never make everything public.
  • One primary View per file; file name == type name. Extract subviews to their own files once a body passes ~60 lines.
  • Naming: views are nouns (ProfileView), observable models are …Model/…Store (ProfileModel), not …ViewModel God objects. Routers are …Router/…Coordinator.
  • Group by feature, not by type. Avoid Views/, Models/, Services/ top-level buckets across the whole app.
  • Localize with String Catalogs (.xcstrings); never hardcode user-facing strings. Reference SF Symbols by name via generated symbol constants where available.
  • Formatting: swift format config committed at repo root; 4-space indent, trailing commas on multiline collections. Run in a pre-commit hook and in CI.
  • Enable SWIFT_STRICT_CONCURRENCY = complete and SWIFT_TREAT_WARNINGS_AS_ERRORS = YES (or -warnings-as-errors) in shared build settings.

Views: small, declarative, composable

  • Keep body pure and cheap. It is recomputed constantly — no network calls, no Date() side effects, no allocation-heavy work, no DispatchQueue in it.
  • Decompose with subview structs and @ViewBuilder computed properties. Do not decompose with functions returning some View when the fragment holds state.
  • Never wrap in AnyView to escape the type system. Use @ViewBuilder, generics, or Group. AnyView kills SwiftUI's diffing and is a code smell.
  • ForEach over Identifiable data with a stable id; never use array indices as identity for mutable collections.
  • Large lists: List (recycles) or LazyVStack/LazyVGrid inside ScrollView. Never a plain VStack for hundreds of rows.
  • Prefer the layout system (Grid, ViewThatFits, containerRelativeFrame, Layout protocol) over nested GeometryReader. Reach for GeometryReader only when you truly need a live size.
  • Attach async work with .task { } (auto-cancels on disappear) and .task(id:) to re-run on input change — not .onAppear { Task { } }.
  • Animate with withAnimation / .animation(_, value:) bound to a specific value. Never the deprecated valueless .animation(_).
  • On iOS 26, recompiling already gives NavigationStack/TabView/toolbars the Liquid Glass look for free. For custom glass use .glassEffect() inside a GlassEffectContainer; never stack glass on glass.

State and data flow (Observation)

  • Model reference state with @Observable classes. Forbidden in new code: ObservableObject, @Published, @StateObject, @ObservedObject, @EnvironmentObject.
    @Observable @MainActor
    final class ProfileModel {
        var user: User?
        private(set) var isLoading = false
        func load() async { … }   // logic lives here, not in the view
    }
    
  • View-local ownership: @State private var model = ProfileModel()@State owns reference types on iOS 17+, replacing @StateObject.
  • Pass models down as plain let for read-only; use @Bindable when a child needs two-way bindings: @Bindable var model then TextField("Name", text: $model.name).
  • @State for view-local value state; derive Bindings with $ and pass them down. Child views take @Binding, not the whole model, when they only mutate one field.
  • Keep all business logic, validation, and side effects in the @Observable model. The view maps model state → views and forwards user intent. Unidirectional: intent in, state out.
  • Models are value-driven: hold struct domain types (User, Order), not mutable class graphs. Mutate by replacing values.
  • Observation is precise — a view only re-renders for the exact properties it reads. Don't hand-split models to "optimize" re-renders; read narrowly instead.

Concurrency (Swift 6, data-race safe)

  • UI types are @MainActor; SwiftUI Views already are. Mark UI-facing @Observable models @MainActor so their mutations are main-actor-isolated by construction.
  • Adopt Swift 6.2 approachable concurrency: set defaultIsolation(MainActor.self) (SwiftPM swiftSettings) so app code is main-actor by default; move heavy/background work off explicitly.
  • Shared mutable state that outlives a view goes in an actor. Don't guard it with locks or serial DispatchQueues.
  • Everything crossing an isolation boundary must be Sendable. Prefer Sendable value types; annotate closures @Sendable. Use sending to hand a non-Sendable value across an isolation boundary by transferring ownership; never silence a warning with @unchecked Sendable or nonisolated(unsafe) — fix the isolation instead.
  • Do background work with Task.detached or a call into an actor/nonisolated async function — never DispatchQueue.global(). Return to the UI simply by being in a @MainActor context.
  • nonisolated for pure, actor-independent helpers so callers don't hop the main actor needlessly.
  • Cancellation: honor it. Check Task.isCancelled / try Task.checkCancellation() in loops; propagate CancellationError. .task cancels for you on disappear.
  • Forbidden: completion-handler pyramids, DispatchQueue.main.async to "get back to UI", DispatchSemaphore, Thread.sleep, and .sink/.assign Combine chains for one-shot async. Use async/await and for await over AsyncSequence.
  • Never block the main thread: no synchronous file/network I/O, no Data(contentsOf:) on a URL, no heavy JSON decode inline. Decode in a background context and await the result.
  • Use NavigationStack with a typed path — NavigationPath for heterogeneous routes or [Route] where Route: Hashable. Never NavigationView (deprecated).
  • Value-based navigation: NavigationLink(value:) + .navigationDestination(for: Route.self) { … }. Do not use NavigationLink(destination:) for programmatic flows — it can't be driven by state and eagerly builds destinations.
  • Own the path in an @Observable router in the Environment; drive push/pop and deep links by mutating router.path. Keep destination mapping in one place.
  • Multi-column (iPad/Mac): NavigationSplitView with explicit column visibility. Adapt, don't fork per-idiom code paths.
  • Sheets/covers via .sheet(item:) / .fullScreenCover(item:) bound to an optional Identifiable, not scattered booleans. Use .presentationDetents for partial sheets.

Dependency injection

  • Inject services through the Environment. Define keys with the @Entry macro — no manual EnvironmentKey conformances:
    extension EnvironmentValues { @Entry var apiClient: APIClient = .live }
    
  • Read with @Environment(\.apiClient) private var apiClient. Provide fakes in previews/tests via .environment(\.apiClient, .mock).
  • No global singletons (Service.shared) reached from view bodies. Compose dependencies at the app root and pass down.
  • Depend on protocols/closures for anything with I/O so it's swappable in tests and previews.

Persistence and networking

  • Local store: SwiftData @Model classes, a single ModelContainer injected via .modelContainer(…), @Query for reactive fetches, #Predicate for typed filtering. Do mutations on the @MainActor context or a background ModelContext.
  • New apps use SwiftData, not Core Data. Only touch Core Data in existing stacks.
  • Networking: URLSession async APIs (data(for:)), Codable DTOs, typed endpoint definitions. No third-party HTTP client unless a real need exists. Decode off the main actor.

Testing

  • Default to Swift Testing: @Test functions, #expect(...) for soft checks, try #require(...) to unwrap-or-fail, @Suite for grouping, @Test(arguments:) for parameterized cases, confirmation for async event assertions.
  • Test @Observable models directly — they're plain classes with no UI dependency. Drive await model.load(), assert on published state. This is where coverage lives.
  • Async tests are native async functions; annotate @MainActor where the model is main-actor-isolated. No expectation/waitFor boilerplate.
  • Inject mock dependencies (mock APIClient, in-memory ModelContainer with isStoredInMemoryOnly: true) so tests are deterministic and offline.
  • Use #Preview for every non-trivial view, including loading/empty/error states, with mock data. Previews are part of the deliverable, not optional.
  • Keep XCUITest for a thin layer of critical end-to-end flows only; they're slow and flaky — don't test logic through the UI.

Accessibility

  • Support Dynamic Type: use semantic fonts (.font(.body), .headline) and never fixed point sizes for text. Verify layout at the largest accessibility sizes; let text reflow with ViewThatFits rather than truncate.
  • Give every actionable or informational non-text view an .accessibilityLabel; combine related elements with .accessibilityElement(children: .combine). Mark purely decorative images .accessibilityHidden(true).
  • Tap targets ≥ 44×44 pt. Gate motion and translucency on @Environment(\.accessibilityReduceMotion) and \.accessibilityReduceTransparency — including Liquid Glass and blur effects.
  • Use semantic and asset-catalog Colors so Dark Mode and Increase Contrast work for free; don't hardcode hex. Audit with the Accessibility Inspector before shipping.

Security

  • Store tokens, keys, and credentials in the Keychain (or a wrapper). Never in UserDefaults, plists, or source.
  • No secrets in the repo. Inject via .xcconfig/CI environment; keep those files git-ignored. Don't hardcode API keys in Swift or Info.plist.
  • HTTPS only; keep App Transport Security on. Do not add NSAllowsArbitraryLoads. For high-value APIs, pin certificates via URLSessionDelegate.
  • Ship an accurate Privacy Manifest (PrivacyInfo.xcprivacy) declaring collected data types and required-reason API usage — mandatory for App Store review.
  • Gate sensitive access with LocalAuthentication (LAContext); mark sensitive files with Data Protection (.completeUnlessOpen).
  • Validate and sanitize deep-link/universal-link inputs before acting on them. Treat all URL parameters as untrusted.
  • Never log PII, tokens, or full request bodies. Use Logger with privacy: .private for anything user-derived.

Do

  • Keep views small and single-purpose; push logic into @Observable models.
  • Use @State/@Bindable/@Environment (Observation) for all state.
  • Write async/await end-to-end; isolate shared mutable state in actors.
  • Use NavigationStack + .navigationDestination(for:) with typed routes.
  • Unwrap with guard let/if let/??; model absence with optionals and typed throws.
  • Prefer struct value types for domain data; make types Sendable.
  • Log with OSLog/Logger, categorized by subsystem.
  • Gate iOS 26-only APIs with if #available(iOS 26, *) and provide a fallback.
  • Run swift format, SwiftLint, and the test suite before finishing.

Avoid

  • ObservableObject + @Published + @StateObject/@ObservedObject/@EnvironmentObject → use @Observable + @State/@Bindable/@Environment.
  • NavigationViewNavigationStack/NavigationSplitView.
  • NavigationLink(destination:) for programmatic flows → value-based .navigationDestination.
  • AnyView for control flow → @ViewBuilder/generics/Group.
  • DispatchQueue.main.async, DispatchSemaphore, completion handlers → async/await, @MainActor, Task.
  • Combine (@Published, sink, assign) for new async pipelines → AsyncSequence/AsyncStream.
  • Force-unwrap ! and try! → optional binding and typed throws.
  • Massive body / logic in views → extracted subviews + model methods.
  • GeometryReader for everything → Grid, ViewThatFits, containerRelativeFrame, Layout.
  • Core Data for new local storage → SwiftData @Model/@Query.
  • print debugging → Logger.
  • Array indices as ForEach identity for mutable data → stable Identifiable ids.
  • CocoaPods/Carthage → Swift Package Manager.

When you code

  • Make small, reviewable diffs scoped to one feature or fix. Don't refactor unrelated files in the same change.
  • Match the existing module layout, naming, and formatting before introducing new patterns.
  • Build with the Swift 6 language mode and treat every concurrency warning as a real bug — fix the isolation, don't silence it with @unchecked Sendable or nonisolated(unsafe).
  • After changes: swift build, swift test (or the Xcode test action), swift format, and SwiftLint must all pass clean. Confirm previews still render.
  • Add/update tests for every model change and every bug fix (regression test first).
  • Ask before: changing the minimum deployment target, adding a third-party dependency, introducing UIKit interop, migrating persistence (Core Data ↔ SwiftData), or altering the module/package boundaries. Otherwise proceed within these rules.

Drop it in your repo

Save these rules as AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules or .github/copilot-instructions.md — your agent instantly codes to the same standard on Swift 6.3 · SwiftUI · iOS 26 · Xcode 26.

Back to top ↑