| State & UI | | |
| New UI screens | SwiftUI | UIKit interop only where Apple APIs require it |
| Observable state (iOS 17+) | @Observable + main actor isolation | Replaces ObservableObject/Published |
| Async work | async/await, structured concurrency | Detached tasks only when intentionally breaking inheritance |
| Unit/integration tests | Swift Testing | Preferred over XCTest for new tests |
| UI automation tests | XCTest / XCUITest | Keep using for UI and performance tests |
| State machine discipline | | |
| Submit guard | guard state == .idle else { return } | Prevents double-tap duplicate submissions in @Observable stores |
| Auto-reset transitions | Task { try await Task.sleep(for: .milliseconds(500)); state = .idle } | Composer/input ready for next action without manual UI reset |
| Minimal state enums | Remove states that can't happen anymore | Dead enum cases produce dead error handling and mislead future readers |
| Networking & resilience | | |
| Network reachability | @Observable singleton + NWPathMonitor | Publish isConnected; disable submit buttons when offline; start monitor in screen .onAppear |
| Agent tooling & build | | |
| Agent tooling (in Xcode) | Xcode native assistant | Current stable is Xcode 26.6 (Swift 6.3); Xcode 27 beta (Swift 6.4, on-device AI code completion) shipped from WWDC26 but is not yet GA — do not build release submissions against a beta SDK |
| Agent tooling (outside Xcode) | XcodeBuildMCP if callable | Otherwise fall back immediately to Apple CLI |
| CLI fallback | xcodebuild, simctl, xcresulttool | Default path when MCP is unavailable or blocked |
| Build / install / stale-app failures | software-ios-runtime-debugging | Use before UI or feature diagnosis |
| XcodeGen projects | scripts/generate-xcodeproj.sh | Must regenerate after adding new Swift files |
| Local-dev launcher pair | scripts/run-local-ios-dev.sh + scripts/stop-local-ios-dev.sh | Two defensive guards (grep env + generated plist for localhost:); persist simulator UDID; never pkill -f Simulator. See quick-reference-extended.md#agent-tooling--build |
.pbxproj-managed projects | Add new files to target membership | Do not assume on-disk Swift files are auto-discovered |
| Generated files / CI landmines | Commit generated outputs or regenerate in CI hook | git add -f for gitignored-folder tracked files; Xcode Cloud ci_post_clone.sh for env shims. See quick-reference-extended.md |
| TARGETED_DEVICE_FAMILY → "1" | Requires fresh archive + ASC build attachment | Changing project.yml alone is insufficient; value is baked into binary. |
| Canvas & visualization | | |
| Canvas data views | Start animatedProgress at 1 | Data loads after .onAppear; starting at 0 leaves Canvas empty |
| Canvas gestures | DragGesture + SpatialTapGesture overlay | Compute hit targets from coordinates, not invisible tap areas |
| Type checker crashes | Split complex views into helper functions | .overlay { if } and tuple ForEach are common triggers |
| StoreKit & billing | | |
| StoreKit 2 subscriptions | @Observable StoreKitManager + TransactionSyncService | transaction.finish() only after backend sync confirms |
| Paywall presentation | .sheet(isPresented:) from any locked screen | Don't navigate to Settings; present modal directly |
| Product pricing display | product.displayPrice from StoreKit | Never hardcode prices; Apple handles locale formatting |
| Promotional offers (rewards) | Server-signed .promotionalOffer() in Product.purchase(options:) | Bridges Stripe credit gaps for Apple-billed users; user must redeem |
| Paid Apps Agreement (#1 invisible blocker) | Verify Active at appstoreconnect.apple.com/business | Silent empty Product.products(for:) results; check before subscription-level diagnosis. Full decision table + timeline in app-store-connect-checklist.md Phase 5. See quick-reference-extended.md |
ASC Missing Metadata | Fill all required fields: prices, localization, review screenshot | Products exist but fields incomplete. Workflow in app-store-connect-checklist.md. See quick-reference-extended.md |
| Review screenshot | sRGB 8-bit RGB PNG 72 DPI, 1284 × 2778 | ASC rejects Display P3 / 16-bit device screenshots; use Pillow profile conversion. See quick-reference-extended.md |
| Accounting currency (backend-denominated) | NumberFormatter + explicit currencyCode + locale = .current | Never String(format: "£%.2f", …); details in quick-reference-extended.md |
| Year/ID display | Text(verbatim:) | Suppress locale number formatting (2026 not 2,026) |
| Immersive screens & sheets | | |
| Immersive viz screens | ZStack { bg; scene; controls; .sheet(persistent) } | No scroll, no card wrappers, sheet peek + bottom padding |
| Persistent sheet | .presentationBackgroundInteraction(.enabled(upThrough: .medium)) | Allows gestures on viz while sheet is visible |
| Viz state: inspect (chart) | Parent @Binding for zoom/drag | Controls strip reads/writes same state |
| Viz state: orbit (3D) | Parent @State + callback | SceneKit drives camera, reports back |
| Viz state: navigate (map) | Internal @State | Zero-latency gesture response for spatial pan/zoom |
| Parent-initiated reset | Reset token (Int incremented by parent) | View .onChange(of: token) resets internal state |
| Viz overlay controls | Picker(.segmented) + Menu | Native controls, not custom material-backed buttons |
| Dense diagram controls | Zoom in/out/reset + semantic filters | When gates, labels, or markers overlap, expose inspection controls before redrawing the whole chart |
| Diagram detail placement | Below-chart summary or persistent sheet | Do not cover the diagram with popups or bottom overlays that block inspection |
| Canvas animation (glow/pulse) | TimelineView(.animation) wrapping Canvas | Gate behind condition to avoid 30fps waste |
| Square Canvas sizing | .aspectRatio(1, contentMode: .fit) | Prevents dead space in taller-than-wide frames |
| Viz export/share | ImageRenderer + proposedSize | Static export view, no gestures or parallax |
| Immersive shared state | @Observable class via @State + @Bindable | Avoids 15+ @Binding prop-drilling |
Year/ID in navigationTitle | String concat: name + " " + String(year) | No verbatim: overload — interpolation resolves to LocalizedStringKey |
| Multiple sheets | Multiple .sheet modifiers on one view | iOS 16.4+; each gated by its own Optional property |
| Grid cell width | .frame(maxWidth: .infinity) inside LazyVGrid cells | Content doesn't stretch automatically; without this, columns are unequal |
| Help/detail row width | .frame(maxWidth: .infinity, alignment: .leading) | Leading VStack rows shrink to intrinsic width unless explicitly stretched, making peer containers uneven |
| Grid cell clip shape | RoundedRectangle not Capsule | Capsule pinches at wide aspect ratios in stretched grid cells |
Deleting .pbxproj files | Remove PBXBuildFile, PBXFileReference, group child, and sources entry | Missing any one leaves stale references or build warnings |
| Sheet swapping | Set item to nil, delay ~350ms, set new value | .sheet(item:) can't swap in one frame; onDismiss + state = loop |
| l10n in plain enums | Call l10n in View body, not enum methods | @MainActor l10n store can't be called from nonisolated enum funcs |
| l10n key coverage | Verify all l10n.text() keys exist in locale JSONs | Fallback strings mask missing keys in the default language; switch language to confirm |
| l10n value coverage | Verify new non-English values are not English fallbacks | Key parity alone is insufficient; generated catalogs can contain English defaults in every locale |
| l10n large-file edits | Use json.load → modify → json.dump for locale JSONs | The Edit tool silently fails on files >500KB; always verify writes programmatically |
| Generated l10n catalogs | Fix upstream source-of-truth, then regenerate | Editing only the generated iOS copy is a stopgap — next regeneration overwrites it |
LocalizationStore.text crash | Patch source-of-truth, regenerate, test | Stack trace in resolvedTemplate = shipped catalog missing key; Swift fallback alone doesn't survive next generation |
| Backend locale propagation | ?locale= query + Accept-Language header on every request | Priority: ?locale= > Accept-Language > stored profile. Call propagateLocaleToAPIClient() on every picker change. See quick-reference-extended.md |
| Locale-aware time formatting | DateFormatter.dateFormat(fromTemplate: "jm", options: 0, locale: locale) | "jm" skeleton respects 12h/24h user override; cache instances in NSCache. See quick-reference-extended.md |
| Backend prose vs structural data | Structural data: client-side enum helpers; prose: server-side translation | Chart positions/names need no backend translation; only generated prose does. See quick-reference-extended.md |
| Interpolated prose template split | Translate prefix/suffix templates; inject runtime values verbatim | Never cache strings with embedded runtime values (explodes cache key space). See quick-reference-extended.md |
| Auth & push | | |
| Sign in with Apple | ASAuthorizationController + CheckedContinuation | Required by Guideline 4.8 if any 3rd-party social login is offered |
| OTP code input | Hidden TextField + .textContentType(.oneTimeCode) | Better than magic links for native; iOS auto-fills from notifications |
Non-@MainActor delegates | nonisolated + MainActor.assumeIsolated | For ASAuthorizationControllerDelegate, MKLocalSearchCompleterDelegate, etc. |
| Push notification categories | Register UNNotificationCategory in didFinishLaunchingWithOptions | Must be set before any notification arrives; match aps.category from backend |
| Badge count (iOS 16+) | try? await UNUserNotificationCenter.current().setBadgeCount(0) | applicationIconBadgeNumber is deprecated; setBadgeCount is async throws |
| Push delegate isolation | @unchecked Sendable + @MainActor async UN delegate | nonisolated async + nested await MainActor.run crashes with _performBlockAfterCATransactionCommitSynchronizes:. See swiftui-observation-concurrency.md and quick-reference-extended.md |
Bare Task { } isolation | Task { @MainActor [weak self] in ... } from @MainActor classes | Bare Task {} runs on global executor; does NOT inherit @MainActor. See swiftui-observation-concurrency.md |
actor vs @MainActor final class | Prefer @MainActor final class when all consumers are @MainActor | Round-trip @MainActor → actor → @MainActor causes post-await tail on wrong executor. See swiftui-observation-concurrency.md |
@Sendable async closure awaited from @MainActor | Retype as @MainActor async closure | Does NOT reliably resume on main; common in SDK RequestExecutor typealiases. See swiftui-observation-concurrency.md |
SCNView in UIViewRepresentable | Build scene once in makeUIView, updateUIView is a no-op | Reassigning scene in updateUIView crashes via CATransaction off-main. See swiftui-observation-concurrency.md |
| Push action routing | Check response.actionIdentifier in didReceive | UNNotificationDefaultActionIdentifier = tap; custom IDs = action buttons; dismiss = no route |
| Push-open ownership, preferences, entitlements, APNs proof, archive gate, backend routing, QA loop | See full rows in quick-reference-extended.md | Archive gate: codesign -d --entitlements must print production; backend: per-device push_environment column authoritative |
| Swift Concurrency crash triage | Symptom-first triage runbook | Private SwiftUI symbol crash → start at swift-concurrency-crash-triage.md; ladder: console → MTC → TSan → lldb bt |
| SwiftUI API modernization | | |
| Deprecated API review | references/swiftui-deprecated-api.md | Systematic deprecated→modern mapping |
| SwiftUI performance audit | references/swiftui-performance.md | View splitting, lazy stacks, modifier efficiency |
| Modern Swift idioms | references/modern-swift-patterns.md | Foundation modernization, date/string/collection patterns |
| Concurrency (constructive) | | |
| Writing correct concurrency | references/swift-concurrency-patterns.md | Structured concurrency, async streams, bridging, migration |
| Concurrency compiler errors | references/swift-concurrency-diagnostics.md | Error→fix mapping for Swift 6 diagnostics |
| Persistence | | |
| SwiftData modeling | references/swiftdata-core.md | Core rules, predicates, CloudKit, indexing, class inheritance |
| Core Data persistence | references/core-data-persistence.md | Stack setup, contexts, object IDs, batch ops, migrations, CloudKit |
| Reusable app skeleton | references/native-ios-app-foundation-skeleton.md | SwiftUI shell, Observation state, persistence, CloudKit, App Intents, local AI hooks, release gates |
| iCloud database app | references/icloud-cloudkit-app-skeleton.md | SwiftData/Core Data/CloudKit choice, private/public/shared scopes, no-server limits |
| Stacks & monetization | | |
| Pick a starter stack to monetize/engage | references/starter-stacks-and-monetization.md | CloudKit→Cloudflare→RevenueCat→Supabase graduation ladder; on-device AI as free tier; webhook idempotency traps |
| Run iOS dev as a conveyor / app factory | references/ios-app-conveyor.md | 4 pillars: default stack per class, shared SPM, Fastlane+Match CI, agent build loop; 2026 stack survey |
| Build performance | | |
| Xcode build optimization | references/xcode-build-optimization.md | Benchmarking, diagnostic flags, SPM analysis, common wins |
| Swift 6.2+ / Xcode 26+ additions | | |
| Default actor isolation (new projects) | SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor in Xcode 26+ | App targets benefit most; library targets stay nonisolated. Flip flag, triage leaf types, mark non-UI nonisolated/@concurrent, remove defensive MainActor.run wraps. Do NOT bulk-silence with @preconcurrency |
withAnimation in @MainActor | Known iOS 26 regression | Hoist withAnimation outside @MainActor body or wrap in Task { @MainActor in withAnimation(…) { … } }; verify on iOS 26.0 / 26.2 / 26.4 |
SubscriptionStatus.all stale after subscription change | Xcode 26 StoreKit 2 bug | Query Product.SubscriptionInfo.Status directly after Transaction.updates tick; don't trust cached all array |
AnyView avoidance | Type erasure defeats diffing | Use @ViewBuilder, some View, or Group + if/switch instead |
@ObservedObject in new code | Legacy pre-iOS-17 pattern | Replace with @Observable + @Bindable; @StateObject → @State; @Published unnecessary on @Observable |
NavigationView deprecation | Replaced | NavigationStack for push/pop; NavigationSplitView for multi-column |
.id(UUID()) force refresh | Anti-pattern | Causes full re-init + retained subscriptions; drive refreshes from state |
| Combine subscription lifecycle | Memory leak #1 | Every sink/assign into .store(in: &cancellables) or cancel in onDisappear/deinit |
| Coordinator-pattern navigation | NavigationStack + enum routes | @Observable AppCoordinator with path; inject via @Entry; one coordinator per module |