slash-do 2.8.1 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/do/better-swift.md +101 -4
- package/install.sh +1 -1
- package/lib/swift-gotchas.md +658 -0
- package/package.json +1 -1
- package/uninstall.sh +1 -1
|
@@ -76,6 +76,7 @@ When compacting during this workflow, always preserve:
|
|
|
76
76
|
- `PHASE_4C_START_SHA` (needed for FILE_OWNER_MAP update in Phase 4c.3)
|
|
77
77
|
- `VACUOUS_TESTS_FIXED`, `WEAK_TESTS_STRENGTHENED`, `NEW_TEST_CASES`, `NEW_TEST_FILES`
|
|
78
78
|
- `CREATED_CATEGORY_SLUGS` (list of branch slugs created in Phase 5)
|
|
79
|
+
- `GOTCHA_ENTRIES_IN_SCOPE` (list of swift-gotchas catalogue entry numbers relevant to this project, recorded in Phase 0e)
|
|
79
80
|
|
|
80
81
|
|
|
81
82
|
## Phase 0: Discovery & Setup
|
|
@@ -115,6 +116,12 @@ Detect additional Swift project characteristics:
|
|
|
115
116
|
- Combine usage (`import Combine`, `@Published`, `AnyPublisher`)
|
|
116
117
|
- Swift concurrency adoption (`async`, `await`, `actor`, `@MainActor`)
|
|
117
118
|
- Widget extensions, App Intents, or other extension targets
|
|
119
|
+
- **CloudKit usage** (`import CloudKit`, `CKContainer`, `cloudKitDatabase:` in `ModelConfiguration`) — flag for Agent 5 lazy-init audit
|
|
120
|
+
- **iCloud entitlements** (`com.apple.developer.icloud-container-identifiers` in `.entitlements`) — flag for Agent 6 ubiquity container audit
|
|
121
|
+
- **Localization** (`Localizable.xcstrings` file present, `String(localized:)` calls, `LocalizedStringKey` parameters) — flag for Agent 6 localization audit
|
|
122
|
+
- **StoreKit / IAPs** (`import StoreKit`, `.storekit` config file, `Product.products(for:)`) — flag for Agent 6 IAP audit
|
|
123
|
+
- **CI/CD release path** (`.github/workflows/*.yml` referencing `apple-actions/upload-testflight-build` or `xcrun altool`) — flag for Agent 6 TestFlight upload validation audit
|
|
124
|
+
- **Code signing in CI** (CI workflow uses `CODE_SIGNING_ALLOWED=NO` for tests) — Agent 5 must aggressively check CloudKit eager-init crash patterns
|
|
118
125
|
|
|
119
126
|
Record as `PROJECT_TYPE` = "SwiftUI" with characteristics map.
|
|
120
127
|
|
|
@@ -172,6 +179,29 @@ Record as `BUILD_CMD` and `TEST_CMD`.
|
|
|
172
179
|
- Check for `.changelogs/` or `.changelog/` directory → `HAS_CHANGELOG`
|
|
173
180
|
- Check for existing `../better-*` worktrees: `git worktree list`. If found, inform the user and ask whether to resume (use existing worktree) or clean up (remove it and start fresh)
|
|
174
181
|
|
|
182
|
+
### 0e: Known Gotchas Catalogue
|
|
183
|
+
|
|
184
|
+
This command ships with a catalogue of real-world Swift / iOS / macOS failure modes at `~/.claude/lib/swift-gotchas.md`. Each entry documents trigger conditions, root cause, the verified fix, and verification steps for a bug that has shipped to production at least once.
|
|
185
|
+
|
|
186
|
+
Before launching audit agents in Phase 1, scan the project for these signals and record which catalogue entries are in scope. Pass this list to each downstream audit agent so they know which entries to consult.
|
|
187
|
+
|
|
188
|
+
| Entry | Catalogue # | Triggers when project has | Audit agent that uses it |
|
|
189
|
+
|-------|-------------|---------------------------|--------------------------|
|
|
190
|
+
| CKContainer eager-init crash | 1 | CloudKit + CI runs `xcodebuild test ... CODE_SIGNING_ALLOWED=NO` | Agent 5 (Bugs) |
|
|
191
|
+
| SwiftData missing inverse relationship | 2 | `@Model` with `@Relationship` properties | Agent 5 (Bugs) + Agent 7 (Tests) |
|
|
192
|
+
| SwiftData CloudKit cross-Apple-ID sharing gap | 3 | SwiftData + `cloudKitDatabase: .automatic` + household/team/share keywords | Agent 4 (Architecture) + Agent 5 (Bugs) |
|
|
193
|
+
| iCloud ubiquity container silent failure | 4 | iCloud entitlement + `url(forUbiquityContainerIdentifier:)` | Agent 5 (Bugs) + Agent 6 (Platform) |
|
|
194
|
+
| iCloud symlink content corruption | 5 | Code mirrors content into `~/Library/Mobile Documents/` paths | Agent 5 (Bugs) |
|
|
195
|
+
| SwiftUI xcstrings localization | 6 | `Localizable.xcstrings` OR `String(localized:)` calls | Agent 6 (Platform) |
|
|
196
|
+
| XcodeGen project generation | 7 | `project.yml` present | Agent 6 (Platform) |
|
|
197
|
+
| TestFlight upload validation | 8 | CI workflow uses `apple-actions/upload-testflight-build` or `xcrun altool` | Agent 6 (Platform) |
|
|
198
|
+
| App Group provisioning auth failure | 9 | App Groups, Push, or extension targets in `.entitlements` | Agent 6 (Platform) |
|
|
199
|
+
| iOS first-IAP submission rejection | 10 | `import StoreKit` AND `Product.products(for:)` calls | Agent 6 (Platform) |
|
|
200
|
+
| `.foregroundStyle(.accentColor)` compile failure | 11 | SwiftUI code using `.foregroundStyle(.accentColor)` | Agent 5 (Bugs) |
|
|
201
|
+
| Keychain test failures (CryptoKit) | 12 | `SecItemAdd`/`SecItemCopyMatching` + symmetric key generation | Agent 5 (Bugs) |
|
|
202
|
+
|
|
203
|
+
Record the matching entry numbers as `GOTCHA_ENTRIES_IN_SCOPE` (e.g., `[1, 2, 6, 7, 8, 11]`). Audit agents in Phase 1 will be instructed to `Read ~/.claude/lib/swift-gotchas.md` once and check each in-scope entry's trigger conditions against the codebase.
|
|
204
|
+
|
|
175
205
|
|
|
176
206
|
<audit_instructions>
|
|
177
207
|
|
|
@@ -179,6 +209,12 @@ Record as `BUILD_CMD` and `TEST_CMD`.
|
|
|
179
209
|
|
|
180
210
|
Project conventions are already in your context. Pass relevant conventions to each agent.
|
|
181
211
|
|
|
212
|
+
Before launching audit agents, load the gotcha catalogue into your context so you can pass relevant entries to each agent:
|
|
213
|
+
|
|
214
|
+
!`cat ~/.claude/lib/swift-gotchas.md`
|
|
215
|
+
|
|
216
|
+
Use `GOTCHA_ENTRIES_IN_SCOPE` (recorded in Phase 0e) to filter which entries are relevant for this project. Pass each downstream agent ONLY the entries that match its category (per the table in Phase 0e), not the whole catalogue.
|
|
217
|
+
|
|
182
218
|
Launch 7 Explore agents in two batches. Each agent must report findings in this format:
|
|
183
219
|
```
|
|
184
220
|
- **[CRITICAL/HIGH/MEDIUM/LOW]** `file:line` - Description. Suggested fix: ... Complexity: Simple/Medium/Complex
|
|
@@ -286,6 +322,10 @@ Skip step 4 if steps 1-3 reveal the code is correct.
|
|
|
286
322
|
- `Task.detached` with `[self]` (strong capture) — use `[weak self]` for cancelable work
|
|
287
323
|
- Keychain operations (`SecItemAdd`/`SecItemCopyMatching`) that silently fail in Simulator test environments — add in-memory key cache as fallback so encrypt-then-decrypt roundtrips don't break in tests
|
|
288
324
|
- `.foregroundStyle(.accentColor)` doesn't compile — `ShapeStyle` has no `.accentColor`; use `Color.accentColor` explicitly
|
|
325
|
+
- **CloudKit eager-init crash in unsigned test builds — gotcha catalogue #1 (CRITICAL):** `CKContainer(identifier:)` does NOT throw or return nil when the iCloud entitlement is missing — it OS-faults via `EXC_BREAKPOINT`/`SIGTRAP`. `CODE_SIGNING_ALLOWED=NO` strips entitlements from sim builds. Any stored property like `private let container = CKContainer(identifier: "iCloud.foo.bar")` runs at object construction, so the moment any code touches a CloudKit singleton (even just to hold a reference), the host app crashes during XCTest bootstrap with "Early unexpected exit, operation never finished bootstrapping." Feature flags do NOT protect against this — construction happens before the flag check. Fix: convert all `CKContainer`/`CKDatabase`/`CKQuerySubscription` stored properties to `lazy var`. Audit all `@MainActor` singletons and `DataStore`-style references for stored CloudKit properties. Severity: **[CRITICAL]** if CI uses `CODE_SIGNING_ALLOWED=NO`. Full catalogue entry: `~/.claude/lib/swift-gotchas.md` § 1.
|
|
326
|
+
- **SwiftData missing inverse relationship crash — gotcha catalogue #2 (CRITICAL):** every `@Relationship` property must have a matching inverse on the target model. Missing inverse causes `ModelContainer` init to throw `SwiftDataError.loadIssueModelContainer` — and crucially, this fails for BOTH persistent AND in-memory configurations. The error message does NOT identify which relationship is broken. Map every `@Relationship` across all `@Model` classes; if a child model declares `var parent: Parent?` then the parent must declare a matching `var children: [Child]? = nil`. SwiftData CAN auto-infer inverses when both sides declare relationships, but it CANNOT create the inverse when only one side declares it. CloudKit (`cloudKitDatabase: .automatic`) requires inverses to be explicit. Full catalogue entry: `~/.claude/lib/swift-gotchas.md` § 2.
|
|
327
|
+
- **iCloud ubiquity container silent failure — gotcha catalogue #4:** `FileManager.url(forUbiquityContainerIdentifier:)` returning a non-nil URL does NOT mean the container is accessible — it only means the entitlement is configured. Pattern to flag: `if let iCloudURL = fm.url(forUbiquityContainerIdentifier: ...) { ... try? fm.createDirectory(...) ... self.dataDirectory = iCloudURL ... }` — the `try?` swallows permission errors and the app silently operates on an inaccessible directory. Required pattern: after `createDirectory`, verify accessibility via `contentsOfDirectory(at:includingPropertiesForKeys:)` inside `do/catch` (not `try?`), and fall back to local Documents on any failure. Full catalogue entry: `~/.claude/lib/swift-gotchas.md` § 4.
|
|
328
|
+
- **SwiftData CloudKit cross-Apple-ID sharing gap — gotcha catalogue #3:** `ModelConfiguration(cloudKitDatabase:)` only has `.private(...)` and `.automatic` — there is NO `.shared` case. Apps that need cross-user collaboration (family/team apps) will silently sync only across the user's own devices. Flag any app that has `cloudKitDatabase: .automatic` AND mentions "household", "family", "team", "shared", or "invite" in code/comments — they likely need a `CKShare`-on-custom-zone overlay pattern. Common compile errors that indicate this gap: `type 'CKShare.Metadata' has no member 'activityType'`, `(saved, _) = try await db.modifyRecords(...)` (modern async API returns dictionaries, not tuples of arrays). Full catalogue entry: `~/.claude/lib/swift-gotchas.md` § 3.
|
|
289
329
|
|
|
290
330
|
### Batch 2 (2 agents after Batch 1 completes):
|
|
291
331
|
|
|
@@ -312,14 +352,40 @@ Skip step 4 if steps 1-3 reveal the code is correct.
|
|
|
312
352
|
|
|
313
353
|
**Build system & project configuration (when XcodeGen/Tuist detected):**
|
|
314
354
|
- `GENERATE_INFOPLIST_FILE: false` with custom Info.plist missing standard keys (`CFBundleIdentifier`, `CFBundleExecutable`, `CFBundlePackageType`) — causes "Missing bundle ID" on simulator install despite correct `PRODUCT_BUNDLE_IDENTIFIER`. Fix: set `GENERATE_INFOPLIST_FILE: true` to let Xcode merge custom keys with generated ones
|
|
315
|
-
- Preview Content directory with `buildPhase: none` excluding Swift files that are needed at runtime (e.g., `PreviewSampleData.swift` used via launch arguments) — only exclude the `.xcassets`, not the whole directory
|
|
316
|
-
- `UILaunchScreen` key manually added to Info.plist but lost on `xcodegen generate` — XcodeGen regenerates the plist from `info.properties` only; put `UILaunchScreen: {}` in `project.yml` not the plist file. Missing this causes iOS letterbox/compatibility mode
|
|
317
|
-
- Info.plist
|
|
318
|
-
-
|
|
355
|
+
- Preview Content directory with `buildPhase: none` excluding Swift files that are needed at runtime (e.g., `PreviewSampleData.swift` used via launch arguments) — only exclude the `.xcassets`, not the whole directory. In Release builds on CI, `DEVELOPMENT_ASSET_PATHS` files may be stripped — move runtime-needed Swift files OUT of `Preview Content/` into the main source tree
|
|
356
|
+
- `UILaunchScreen` key manually added to Info.plist but lost on `xcodegen generate` — XcodeGen regenerates the plist from `info.properties` only; put `UILaunchScreen: {}` in `project.yml` not the plist file. Missing this causes iOS letterbox/compatibility mode (tiny centered window, large black borders). Also remove `INFOPLIST_KEY_UILaunchScreen_Generation: true` if both are present — they create a nested `UILaunchScreen > UILaunchScreen` structure
|
|
357
|
+
- **Never manually edit the Info.plist file when using XcodeGen's `info.path`** — `xcodegen generate` overwrites the plist from scratch using only `info.properties`. Any custom keys must live in `project.yml` or they're silently deleted
|
|
358
|
+
- Info.plist keys required for TestFlight upload that don't cause build failures (these are rejected SERVER-SIDE by `altool`, not at build time):
|
|
359
|
+
- `UISupportedInterfaceOrientations` must include all 4 orientations for iPad multitasking, OR declare `UIRequiresFullScreen: true` (even iPhone-only apps need this — error code 409). For XcodeGen: set `INFOPLIST_KEY_UISupportedInterfaceOrientations` and `INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad` build settings
|
|
360
|
+
- `CFBundleDocumentTypes` declared without `LSSupportsOpeningDocumentsInPlace` — currently a warning (code 90737), may become a fatal error
|
|
361
|
+
- CI upload actions (`apple-actions/upload-testflight-build@v3`) that report success even when `altool` returns "UPLOAD FAILED" in XML plist output — flag any release workflow that doesn't grep raw upload logs for `UPLOAD FAILED|product-errors|product-warnings`. Always check TestFlight after CI completes to confirm the build actually arrived
|
|
362
|
+
- **Adding new capabilities to an existing app — gotcha catalogue #9:** `xcodebuild archive -allowProvisioningUpdates` may fail with misleading "Authentication failed: Make sure a bearer token was provided" when adding App Groups, Push, etc. — the App Store Connect API key has upload permissions but NOT provisioning profile management permissions. Flag CI workflows that add new entitlements without corresponding documentation that someone has manually fixed provisioning in Xcode GUI first
|
|
363
|
+
- For XcodeGen multi-platform targets with widget extensions, the widget dependency needs `platformFilter: iOS` since widgets are primarily iOS
|
|
364
|
+
- Build system & TestFlight gotchas — full catalogue entries: `~/.claude/lib/swift-gotchas.md` § 7 (XcodeGen), § 8 (TestFlight upload), § 9 (App Group provisioning)
|
|
319
365
|
|
|
320
366
|
**iCloud & data persistence (when iCloud entitlements detected):**
|
|
321
367
|
- `url(forUbiquityContainerIdentifier:)` returning non-nil does NOT mean the container is accessible — always verify with `createDirectory` + `contentsOfDirectory` using `do/catch` (not `try?`) and fall back to local Documents directory on failure
|
|
322
368
|
- `try?` on iCloud file operations silently swallowing permission errors — app appears to work but reads/writes to inaccessible path with empty results
|
|
369
|
+
- Sparse/dehydrated iCloud files: any directory that was symlinked or rsynced into iCloud Drive can produce files with `st_size > 0` but `st_blocks = 0` that read as empty. Flag any code that mirrors content into iCloud paths without integrity verification (see catalogue § 5)
|
|
370
|
+
|
|
371
|
+
**Localization & String Catalogs (when `Localizable.xcstrings` or `String(localized:)` detected):**
|
|
372
|
+
- **`Text(someStringVariable)` silent failure** — `Text("literal")` accepts `LocalizedStringKey` and auto-localizes from the string catalog, but `Text(someVariable)` where `someVariable: String` does NOT — it renders raw. Any reusable component accepting `title: String` and passing it to `Text(title)` ships untranslated UI unless callers pre-localize via `String(localized:)`. Either change the parameter type to `LocalizedStringKey` (for literal-only callers) or document that callers must pre-localize. This is the #1 silent localization failure
|
|
373
|
+
- **`String(localized:)` ignores SwiftUI environment locale** — it uses `Bundle.main`'s preferred language, which is controlled by `UserDefaults["AppleLanguages"]`, not `.environment(\.locale, ...)`. Apps with in-app language pickers must either (a) restart after writing to `AppleLanguages`, or (b) build an `appLocalized()` helper that passes the user's chosen locale explicitly via `String(localized: key, locale: .app, comment: ...)`
|
|
374
|
+
- **AGA `^[...](inflect: true)` requires `LocalizedStringKey`** — Apple's Automatic Grammar Agreement only fires when rendered as `Text(LocalizedStringKey)`. `String(localized: "^[\(count) horse](inflect: true)")` returns a plain `String`, strips the AGA pipeline, and renders the markup literally. AGA `inflect: true` is also unreliable for non-English locales — use explicit `variations.plural` (`one`/`other` keys) in xcstrings for ALL locales including English
|
|
375
|
+
- **Static cached `DateFormatter` for locale-dependent formats** — `private static let formatter = DateFormatter()` captures the locale at first init and never updates; in-app language switches won't change rendered dates. Use `Date.FormatStyle` (e.g., `date.formatted(.dateTime.month().day())`) which respects current locale, OR construct a fresh formatter per call. Static caching IS safe for locale-independent formats like `yyyyMMdd` (set `locale = Locale(identifier: "en_US_POSIX")` to prevent calendar interference)
|
|
376
|
+
- **`date.formatted(...)` vs `Text(date, format:)` in views** — the former returns a `String` using `Locale.current` (system locale, non-reactive); the latter uses the SwiftUI environment locale and IS reactive to `.environment(\.locale, ...)` changes. Prefer `Text(date, format: ...)` in view bodies
|
|
377
|
+
- **Localized strings stored in SwiftData/CoreData** — flag any `@Model` property that holds a translated display string instead of a raw enum value. Storing `"Lektion"` in German and reading in French gives mixed-language UI. Always store raw values (`categoryRaw: String = "lesson"`) and compute `displayName` at render time
|
|
378
|
+
- **Missing `comment:` parameter on `String(localized:)` calls** — translators have no context, and the xcstrings extraction tool won't auto-populate hints
|
|
379
|
+
- **Missing `en` entry in xcstrings when other languages are present** — causes English strings to display with raw `^[...]` markup or fall back to keys
|
|
380
|
+
- **Tab bar / sidebar / navigation labels not translating** — usually caused by Pattern 1 vs 2 confusion: `Label(category.displayName, ...)` won't translate unless `displayName` returns a pre-localized `String`
|
|
381
|
+
- Full catalogue entry with all 8 sub-gotchas and the `appLocalized()` helper template: `~/.claude/lib/swift-gotchas.md` § 6
|
|
382
|
+
|
|
383
|
+
**In-App Purchases & StoreKit (when StoreKit imports detected):**
|
|
384
|
+
- **Missing "Restore Purchases" button (Guideline 3.1.1 — auto-rejection):** apps that offer IAPs MUST include a distinct, user-tappable Restore button on the same screen where IAPs are shown, not buried in deep settings. Implementation: `try await AppStore.sync()` (StoreKit 2), then refresh purchase state. Show loading state during restore, alerts on success/failure
|
|
385
|
+
- **Hardcoded fallback price in PurchaseButton** — `Text(price ?? "$0.99")` shows a tappable button when products haven't loaded, leading to "Unable to make IAP purchases" in App Review. Fix: show `ProgressView()` until `Product.products(for:)` resolves, then enable the button
|
|
386
|
+
- **First-time IAPs in TestFlight cannot be tested** — `product.purchase()` will fail with "Unable to Complete Request" for IAPs that have never been approved by App Review, even though `Product.products(for:)` returns prices correctly. This is a sandbox limitation, not a code bug. Flag any project README or testing doc that says "test IAPs in TestFlight" without mentioning the local `.storekit` configuration file workflow (only works in Xcode debug runs, not archived/TestFlight builds)
|
|
387
|
+
- **`@available` annotations not matching actual StoreKit version** — StoreKit 2 (`Product`, `Transaction`, `AppStore.sync()`) requires iOS 15+/macOS 12+. Mixing StoreKit 1 (`SKProduct`, `SKPaymentQueue`) and StoreKit 2 in the same purchase flow without clear version gating produces surprising bugs
|
|
388
|
+
- Full catalogue entry with submission flow (clear "Rejected" IAP localizations, submit IAPs alongside an app version, local `.storekit` testing): `~/.claude/lib/swift-gotchas.md` § 10
|
|
323
389
|
|
|
324
390
|
**SwiftUI best practices (ALL projects):**
|
|
325
391
|
- Deprecated APIs: `NavigationView` (use `NavigationStack`/`NavigationSplitView`), `onChange(of:perform:)` one-parameter form (use two-parameter), `.onAppear` for async work (use `.task`)
|
|
@@ -365,6 +431,18 @@ Skip step 4 if steps 1-3 reveal the code is correct.
|
|
|
365
431
|
- Missing `XCUITest` for critical navigation flows and platform-specific interactions
|
|
366
432
|
- Missing preview coverage: all views should have `#Preview` for each platform × Dark Mode × Dynamic Type extremes
|
|
367
433
|
- Missing error path tests for network failures, decode failures, and permission denials
|
|
434
|
+
- **Missing `testModelContainerSchemaIsValid()` test** when `@Model` classes are present — every project using SwiftData should construct an in-memory `ModelContainer` with ALL model types in a unit test. This catches missing inverse relationships before they reach production (the actual error message gives no hint which relationship is broken). Required pattern:
|
|
435
|
+
```swift
|
|
436
|
+
func testModelContainerSchemaIsValid() throws {
|
|
437
|
+
_ = try ModelContainer(
|
|
438
|
+
for: ModelA.self, ModelB.self, /* every @Model type */,
|
|
439
|
+
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
- **Missing CloudKit lazy-init verification test** — when the project uses CloudKit, add a smoke test that runs the host app under `CODE_SIGNING_ALLOWED=NO` and verifies tests bootstrap successfully. If `CKContainer(identifier:)` is held in any stored property, the host app traps before any test runs
|
|
444
|
+
- **Missing localization round-trip tests** when `Localizable.xcstrings` is present — for each enum with a `displayName` property, verify it returns a non-empty `String` (not the raw key) for at least one supported locale. This catches `Text(stringVariable)` vs `Text(LocalizedStringKey)` regressions
|
|
445
|
+
- **Missing IAP product loading test** when StoreKit is imported — verify `Product.products(for: identifiers)` returns the expected set against a `.storekit` configuration file. This catches typos in product identifiers before they reach App Review
|
|
368
446
|
|
|
369
447
|
**Vacuous tests (tests that don't actually test anything):**
|
|
370
448
|
- Tests that assert on mocked return values instead of real behavior (testing the mock, not the code)
|
|
@@ -528,6 +606,25 @@ SWIFT-SPECIFIC GUARDRAILS:
|
|
|
528
606
|
- Use Swift concurrency (async/await, actors) over GCD/Combine for new code
|
|
529
607
|
- Never introduce AnyView — use @ViewBuilder or Group instead
|
|
530
608
|
- Test both light and dark color schemes when modifying colors
|
|
609
|
+
|
|
610
|
+
GOTCHA CATALOGUE — REQUIRED READING:
|
|
611
|
+
Before fixing any issue tagged with a gotcha catalogue entry number (#1–#12), READ the corresponding entry in `~/.claude/lib/swift-gotchas.md`. Each entry documents the verified fix from a prior project that actually shipped — apply the fix as written rather than improvising. The relevant entries for this project are: {GOTCHA_ENTRIES_IN_SCOPE}.
|
|
612
|
+
|
|
613
|
+
The catalogue covers:
|
|
614
|
+
#1 CKContainer eager-init crash in unsigned test builds (CloudKit + CI)
|
|
615
|
+
#2 SwiftData missing inverse relationship crash (@Model + @Relationship)
|
|
616
|
+
#3 SwiftData CloudKit cross-Apple-ID sharing gap (.automatic + sharing)
|
|
617
|
+
#4 iCloud ubiquity container silent failure (iCloud entitlement)
|
|
618
|
+
#5 iCloud symlink content corruption (iCloud Drive mirroring)
|
|
619
|
+
#6 SwiftUI xcstrings localization silent failures (Localizable.xcstrings)
|
|
620
|
+
#7 XcodeGen project generation gotchas (project.yml)
|
|
621
|
+
#8 TestFlight upload validation gotchas (CI release path)
|
|
622
|
+
#9 xcodebuild App Group provisioning auth failure (new entitlements)
|
|
623
|
+
#10 iOS first-IAP submission rejection (StoreKit / IAPs)
|
|
624
|
+
#11 .foregroundStyle(.accentColor) compile failure (any SwiftUI)
|
|
625
|
+
#12 Keychain test failures in simulator (CryptoKit) (SecItem* + symmetric keys)
|
|
626
|
+
|
|
627
|
+
When a finding cites a catalogue entry, READ that entry in `~/.claude/lib/swift-gotchas.md` first, then apply the FIX section as the remediation. Do not paraphrase the fix — these are load-bearing patterns where minor variations regress the bug.
|
|
531
628
|
```
|
|
532
629
|
|
|
533
630
|
### Conflict avoidance:
|
package/install.sh
CHANGED
|
@@ -55,7 +55,7 @@ OLD_COMMANDS=(cam good makegoals makegood optimize-md)
|
|
|
55
55
|
|
|
56
56
|
LIBS=(
|
|
57
57
|
code-review-checklist copilot-review-loop graphql-escaping
|
|
58
|
-
remediation-agent-template swift-review-checklist
|
|
58
|
+
remediation-agent-template swift-review-checklist swift-gotchas
|
|
59
59
|
review-surface-scan review-security-audit review-cross-file-tracing
|
|
60
60
|
)
|
|
61
61
|
|
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
swift-gotchas.md — Reference catalogue of real-world Swift / iOS / macOS failure modes
|
|
3
|
+
consumed by /do:better-swift audit and remediation agents.
|
|
4
|
+
|
|
5
|
+
Each entry is self-contained: TRIGGER (how to detect it in code/CI),
|
|
6
|
+
ROOT CAUSE (why it happens), FIX (the verified remediation), and
|
|
7
|
+
VERIFY (how to confirm the fix works).
|
|
8
|
+
|
|
9
|
+
Audit agents: scan the project for the TRIGGER signals listed in each entry.
|
|
10
|
+
Remediation agents: when you encounter an issue matching one of these entries,
|
|
11
|
+
apply the FIX as written — do not improvise unless the code has diverged.
|
|
12
|
+
|
|
13
|
+
These are NOT speculative best practices. Every entry below is a bug that
|
|
14
|
+
shipped to production at least once and caused real damage. Treat them as
|
|
15
|
+
load-bearing checks, not style preferences.
|
|
16
|
+
-->
|
|
17
|
+
|
|
18
|
+
# Swift / iOS / macOS Gotcha Catalogue
|
|
19
|
+
|
|
20
|
+
## Quick index
|
|
21
|
+
|
|
22
|
+
| # | Failure mode | Detect when |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| 1 | CKContainer eager-init crash in unsigned test builds | Project uses CloudKit AND CI runs `xcodebuild test ... CODE_SIGNING_ALLOWED=NO` |
|
|
25
|
+
| 2 | SwiftData missing inverse relationship crash | Project has `@Model` classes with `@Relationship` properties |
|
|
26
|
+
| 3 | SwiftData CloudKit cross-Apple-ID sharing gap | SwiftData with `cloudKitDatabase: .automatic` AND household/team/family/share keywords |
|
|
27
|
+
| 4 | iCloud ubiquity container silent failure | iCloud entitlement AND `url(forUbiquityContainerIdentifier:)` calls |
|
|
28
|
+
| 5 | iCloud symlink content corruption | Code mirrors content into `~/Library/Mobile Documents/` paths |
|
|
29
|
+
| 6 | SwiftUI xcstrings localization silent failures | `Localizable.xcstrings` file present OR `String(localized:)` calls |
|
|
30
|
+
| 7 | XcodeGen project generation gotchas | `project.yml` (XcodeGen) detected |
|
|
31
|
+
| 8 | TestFlight upload validation gotchas | CI workflow uploads to TestFlight via `apple-actions/upload-testflight-build` or `xcrun altool` |
|
|
32
|
+
| 9 | xcodebuild App Group provisioning auth failure | App Groups, Push, or extension targets in `.entitlements` |
|
|
33
|
+
| 10 | iOS first-IAP submission rejection | `import StoreKit` AND `Product.products(for:)` calls |
|
|
34
|
+
| 11 | `.foregroundStyle(.accentColor)` compile failure | Any SwiftUI code using `.foregroundStyle(.accentColor)` |
|
|
35
|
+
| 12 | Keychain test failures in simulator | `SecItemAdd` / `SecItemCopyMatching` used for symmetric keys |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 1. CKContainer eager-init crash in unsigned test builds
|
|
40
|
+
|
|
41
|
+
### Trigger
|
|
42
|
+
- `xcodebuild test ... CODE_SIGNING_ALLOWED=NO` fails with: `Early unexpected exit, operation never finished bootstrapping - no restart will be attempted` and `The test runner crashed before establishing connection`
|
|
43
|
+
- Crash report shows `EXC_BREAKPOINT (SIGTRAP)` with frames in `CKContainer.__allocating_init(identifier:)`
|
|
44
|
+
- CloudKit emits a "Significant issue" log: `In order to use CloudKit, your process must have a com.apple.developer.icloud-services entitlement`
|
|
45
|
+
- Tests pass with full code signing but fail with `CODE_SIGNING_ALLOWED=NO`
|
|
46
|
+
- All CloudKit usage IS gated behind a feature flag, yet the app still crashes at launch
|
|
47
|
+
|
|
48
|
+
### Root cause
|
|
49
|
+
`CKContainer(identifier:)` does **not** throw or return nil when the iCloud entitlement is missing — it traps the process via an OS-level fault (`brk 1`). `CODE_SIGNING_ALLOWED=NO` strips entitlements from the simulator build. A stored property like `private let container = CKContainer(...)` runs its initializer the moment the enclosing object is constructed, so any code that touches the singleton (even just to hold a reference) triggers the trap. Feature flags do not protect against this — construction happens before any flag check.
|
|
50
|
+
|
|
51
|
+
### Fix
|
|
52
|
+
Convert every CloudKit stored property to `lazy var`:
|
|
53
|
+
|
|
54
|
+
```swift
|
|
55
|
+
final class CloudKitSync {
|
|
56
|
+
static let shared = CloudKitSync()
|
|
57
|
+
|
|
58
|
+
// Lazy so CKContainer is only constructed when CloudKit is actually used.
|
|
59
|
+
// CKContainer(identifier:) traps when the iCloud entitlement is missing
|
|
60
|
+
// (e.g. unsigned test builds with CODE_SIGNING_ALLOWED=NO).
|
|
61
|
+
private lazy var container = CKContainer(identifier: "iCloud.foo.bar")
|
|
62
|
+
private var privateDB: CKDatabase { container.privateCloudDatabase }
|
|
63
|
+
|
|
64
|
+
private init() {}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`lazy` requires `var`, not `let`. Safe because the property is private and the singleton is `@MainActor`-isolated. Same fix applies to `CKDatabase`, `CKQuerySubscription`, `CKRecordZone`, and any other CloudKit type whose initializer touches the container.
|
|
69
|
+
|
|
70
|
+
Audit checklist:
|
|
71
|
+
- Every stored property of type `CKContainer`, `CKDatabase`, `CKQuerySubscription`, `CKRecordZone` must be `lazy var` or computed
|
|
72
|
+
- No app-launch code path (`App.init`, `@StateObject` default value, eager singleton ref) calls a method on the singleton that touches the lazy container
|
|
73
|
+
|
|
74
|
+
### Verify
|
|
75
|
+
```bash
|
|
76
|
+
# Pick an available iOS Simulator dynamically (no hardcoded device name)
|
|
77
|
+
SIM_DEST=$(xcrun simctl list devices available -j \
|
|
78
|
+
| python3 -c 'import json,sys; d=json.load(sys.stdin)["devices"]; \
|
|
79
|
+
runtimes=sorted([k for k in d if "iOS" in k], reverse=True); \
|
|
80
|
+
sims=next((d[k] for k in runtimes if d[k]), []); \
|
|
81
|
+
print("platform=iOS Simulator,id=" + sims[0]["udid"]) if sims else sys.exit(1)')
|
|
82
|
+
|
|
83
|
+
xcodebuild test \
|
|
84
|
+
-project YourApp.xcodeproj \
|
|
85
|
+
-scheme YourApp \
|
|
86
|
+
-destination "$SIM_DEST" \
|
|
87
|
+
-configuration Debug \
|
|
88
|
+
CODE_SIGNING_ALLOWED=NO \
|
|
89
|
+
-only-testing:YourAppTests
|
|
90
|
+
```
|
|
91
|
+
Tests should build, launch, and execute. Production signed builds remain unaffected — the `lazy` change is transparent.
|
|
92
|
+
|
|
93
|
+
### Notes
|
|
94
|
+
- `try?` around `CKContainer(identifier:)` does NOT help — the init is not `throws`, it OS-faults
|
|
95
|
+
- This crash is silent in `-quiet` mode — re-run without `-quiet` and grep for `CK]`, `EXC_`, or `Significant issue`
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 2. SwiftData missing inverse relationship crash
|
|
100
|
+
|
|
101
|
+
### Trigger
|
|
102
|
+
- `ModelContainer(for:configurations:)` throws `SwiftDataError._Error.loadIssueModelContainer`
|
|
103
|
+
- The error occurs for BOTH persistent AND in-memory `ModelConfiguration` (rules out migration / CloudKit / corrupted store)
|
|
104
|
+
- Recently added a new `@Model` class with a `@Relationship` to an existing model
|
|
105
|
+
- The error message does NOT identify which model or relationship is broken
|
|
106
|
+
|
|
107
|
+
### Root cause
|
|
108
|
+
SwiftData CAN auto-infer inverse relationships when both sides declare them, but it CANNOT create the inverse property when only one side does. Every `@Relationship` property must have a matching declaration on the target model. CloudKit (`cloudKitDatabase: .automatic`) makes the requirement even stricter — all relationships must have explicit inverses.
|
|
109
|
+
|
|
110
|
+
### Fix
|
|
111
|
+
1. Map all relationships across every `@Model` class:
|
|
112
|
+
|
|
113
|
+
| Model A property | Model B property | Status |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `Horse.vetRecords: [VetRecord]?` | `VetRecord.horse: Horse?` | OK — auto-inferred |
|
|
116
|
+
| `BarnNote.horse: Horse?` | _(nothing on Horse)_ | BROKEN — missing inverse |
|
|
117
|
+
|
|
118
|
+
2. Add the missing inverse on the target model:
|
|
119
|
+
```swift
|
|
120
|
+
// Horse.swift — ADD THIS:
|
|
121
|
+
@Relationship(deleteRule: .cascade)
|
|
122
|
+
var barnNotes: [BarnNote]? = nil
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
3. Delete the app from the simulator/device to clear any corrupted local store, then rebuild.
|
|
126
|
+
|
|
127
|
+
4. Add this unit test so the issue can never silently regress:
|
|
128
|
+
```swift
|
|
129
|
+
func testModelContainerSchemaIsValid() throws {
|
|
130
|
+
_ = try ModelContainer(
|
|
131
|
+
for: ModelA.self, ModelB.self, /* every @Model type */,
|
|
132
|
+
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Verify
|
|
138
|
+
The unit test above passes. If the in-memory container initializes successfully, the schema is valid — any remaining persistent-store errors are migration issues, not schema issues.
|
|
139
|
+
|
|
140
|
+
### Diagnostic flow when you see `loadIssueModelContainer`
|
|
141
|
+
1. It's NOT a migration issue (in-memory stores don't migrate)
|
|
142
|
+
2. It's NOT a CloudKit issue (in-memory stores don't use CloudKit)
|
|
143
|
+
3. It IS a schema definition issue — check ALL `@Relationship` properties
|
|
144
|
+
4. Use `git log` to find the commit that added the new model and verify both sides were updated
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 3. SwiftData CloudKit cross-Apple-ID sharing gap
|
|
149
|
+
|
|
150
|
+
### Trigger
|
|
151
|
+
- App uses `cloudKitDatabase: .automatic` and needs spouse / family member / teammate (different Apple ID) to collaborate
|
|
152
|
+
- Compiler error: `type 'CKShare.Metadata' has no member 'activityType'` or `activityTypeKey`
|
|
153
|
+
- Compiler error: `(saved, _) = try await db.modifyRecords(...)` doesn't compile
|
|
154
|
+
- `ModelConfiguration` has no `cloudKitDatabase: .shared` case
|
|
155
|
+
- Spouse/teammate logs in on a different Apple ID and sees an empty database despite the inviter having data
|
|
156
|
+
|
|
157
|
+
### Root cause
|
|
158
|
+
SwiftData's `cloudKitDatabase: .automatic` only syncs the user's own private database across the user's own devices. There is no `.shared` option. Apple's official answer is "drop SwiftData and use `NSPersistentCloudKitContainer` directly" — but you can keep SwiftData and overlay a `CKShare` on a custom `CKRecordZone` instead.
|
|
159
|
+
|
|
160
|
+
### Fix
|
|
161
|
+
Architecture: keep SwiftData as the local source of truth, overlay a `CKShare` on a custom `CKRecordZone` per shareable root, and let the CKShare handshake piggyback on the same CloudKit container SwiftData is already using.
|
|
162
|
+
|
|
163
|
+
Step 1 — add a stable zone identifier to your shareable root model:
|
|
164
|
+
```swift
|
|
165
|
+
@Model
|
|
166
|
+
final class Household {
|
|
167
|
+
var name: String = ""
|
|
168
|
+
var cloudZoneName: String = "" // ← stable per-household zone ID
|
|
169
|
+
var ownerUserRecordName: String? // nil = local user owns it
|
|
170
|
+
var shareIsActive: Bool = false
|
|
171
|
+
|
|
172
|
+
init(name: String) {
|
|
173
|
+
self.name = name
|
|
174
|
+
self.cloudZoneName = "Household-\(UUID().uuidString)"
|
|
175
|
+
}
|
|
176
|
+
var isOwner: Bool { ownerUserRecordName == nil }
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Step 2 — sharing service. Two non-obvious bits:
|
|
181
|
+
1. The modern async `db.modifyRecords(saving:deleting:)` returns DICTIONARIES, not arrays:
|
|
182
|
+
```swift
|
|
183
|
+
let result = try await privateDB.modifyRecords(saving: [rootRecord, share], deleting: [])
|
|
184
|
+
// result.saveResults is [CKRecord.ID: Result<CKRecord, Error>]
|
|
185
|
+
let savedShare = result.saveResults.values.compactMap { res -> CKShare? in
|
|
186
|
+
if case .success(let record) = res { return record as? CKShare }
|
|
187
|
+
return nil
|
|
188
|
+
}.first
|
|
189
|
+
```
|
|
190
|
+
2. `container.accept([metadata])` returns a value the compiler will warn about — bind it explicitly:
|
|
191
|
+
```swift
|
|
192
|
+
_ = try await container.accept([metadata])
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Step 3 — accept incoming shares. The activity type is the well-known string `"com.apple.CloudKit.ShareMetadata"` and the metadata lives under `"CKShareMetadata"` in `userInfo`. **There is NO `CKShare.Metadata.activityType` constant** even though older Apple sample code references one. Hardcode the strings:
|
|
196
|
+
```swift
|
|
197
|
+
WindowGroup {
|
|
198
|
+
ContentView()
|
|
199
|
+
.modelContainer(container)
|
|
200
|
+
.onContinueUserActivity("com.apple.CloudKit.ShareMetadata") { activity in
|
|
201
|
+
guard let metadata = activity.userInfo?["CKShareMetadata"] as? CKShare.Metadata else { return }
|
|
202
|
+
Task { @MainActor in
|
|
203
|
+
_ = try? await CloudKitSharingService.shared.acceptShare(metadata: metadata)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Step 4 — SceneDelegate fallback. The SwiftUI `onContinueUserActivity` callback is unreliable on cold launch (iOS 17/18). Implement BOTH paths if your app supports cold-launch share acceptance:
|
|
210
|
+
```swift
|
|
211
|
+
// UIWindowSceneDelegate
|
|
212
|
+
func windowScene(_ windowScene: UIWindowScene,
|
|
213
|
+
userDidAcceptCloudKitShareWith metadata: CKShare.Metadata) {
|
|
214
|
+
Task { @MainActor in
|
|
215
|
+
try? await CloudKitSharingService.shared.acceptShare(metadata: metadata)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
On macOS: `application(_:userDidAcceptCloudKitShareWith:)` on `NSApplicationDelegate`.
|
|
220
|
+
|
|
221
|
+
Step 5 — owner records live in `privateCloudDatabase`, accepted shares live in `sharedCloudDatabase`. SwiftData's CloudKit mirror handles both automatically once accepted, but if you query CKRecords directly you must pick the right database.
|
|
222
|
+
|
|
223
|
+
### Verify
|
|
224
|
+
1. Owner-side: create a household, tap Invite. `UICloudSharingController` opens with a share URL. CloudKit Dashboard → Zones shows a new `Household-<UUID>` zone with a `HouseholdRoot` record containing a `cloudkit.share` field.
|
|
225
|
+
2. Member-side: send the URL to a different Apple ID via Messages. Tap the URL. Your `onContinueUserActivity` (or scene delegate) callback fires with metadata.
|
|
226
|
+
3. Sync test: owner adds a child entity → member sees it within ~10 seconds. (No manual record mirroring.)
|
|
227
|
+
4. To ship to TestFlight you must hit **Deploy Schema Changes…** in CloudKit Console to promote custom record types to PRODUCTION.
|
|
228
|
+
|
|
229
|
+
### Notes
|
|
230
|
+
- `@MainActor` deinit gotcha: if you make the sharing service `@MainActor`, you cannot reference any main-actor-isolated stored properties from `deinit`. Just delete the deinit (singletons live forever).
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 4. iCloud ubiquity container silent failure
|
|
235
|
+
|
|
236
|
+
### Trigger
|
|
237
|
+
- App has `com.apple.developer.icloud-container-identifiers` entitlement
|
|
238
|
+
- `FileManager.url(forUbiquityContainerIdentifier:)` returns a non-nil URL
|
|
239
|
+
- App shows empty state (0 items) despite files in local Documents directory
|
|
240
|
+
- Import operations appear to succeed but data is never persisted
|
|
241
|
+
- Removing `try?` reveals: "You don't have permission to save the file 'foo' in the folder 'Documents'"
|
|
242
|
+
|
|
243
|
+
### Root cause
|
|
244
|
+
`url(forUbiquityContainerIdentifier:)` returning non-nil only means the entitlement is **configured** — NOT that the container is ready for use. `try?` then swallows the actual permission error, leaving the app silently operating on an inaccessible directory.
|
|
245
|
+
|
|
246
|
+
### Fix
|
|
247
|
+
Verify-then-use pattern with fallback. Never trust the URL alone:
|
|
248
|
+
```swift
|
|
249
|
+
private init() {
|
|
250
|
+
let fm = FileManager.default
|
|
251
|
+
let resolvedDir: URL
|
|
252
|
+
let resolvedICloud: Bool
|
|
253
|
+
|
|
254
|
+
if let iCloudURL = fm.url(forUbiquityContainerIdentifier: "iCloud.com.example.App") {
|
|
255
|
+
let targetDir = iCloudURL.appendingPathComponent("Documents/data")
|
|
256
|
+
var iCloudWorks = false
|
|
257
|
+
do {
|
|
258
|
+
try fm.createDirectory(at: targetDir, withIntermediateDirectories: true)
|
|
259
|
+
// Critical: verify the directory is actually accessible
|
|
260
|
+
_ = try fm.contentsOfDirectory(at: targetDir, includingPropertiesForKeys: nil)
|
|
261
|
+
iCloudWorks = true
|
|
262
|
+
} catch {
|
|
263
|
+
// iCloud container exists but isn't accessible (permission denied, etc.)
|
|
264
|
+
}
|
|
265
|
+
if iCloudWorks {
|
|
266
|
+
resolvedDir = targetDir
|
|
267
|
+
resolvedICloud = true
|
|
268
|
+
} else {
|
|
269
|
+
// Fall back to local Documents
|
|
270
|
+
let docs = fm.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
|
271
|
+
let local = docs.appendingPathComponent("data")
|
|
272
|
+
try? fm.createDirectory(at: local, withIntermediateDirectories: true)
|
|
273
|
+
resolvedDir = local
|
|
274
|
+
resolvedICloud = false
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
let docs = fm.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
|
278
|
+
let local = docs.appendingPathComponent("data")
|
|
279
|
+
try? fm.createDirectory(at: local, withIntermediateDirectories: true)
|
|
280
|
+
resolvedDir = local
|
|
281
|
+
resolvedICloud = false
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
self.dataDirectory = resolvedDir
|
|
285
|
+
self.isICloud = resolvedICloud
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Anti-pattern to flag during audit:
|
|
290
|
+
```swift
|
|
291
|
+
// BAD: assumes non-nil URL means the directory is usable
|
|
292
|
+
if let iCloudURL = fm.url(forUbiquityContainerIdentifier: "...") {
|
|
293
|
+
let dir = iCloudURL.appendingPathComponent("Documents/data")
|
|
294
|
+
try? fm.createDirectory(at: dir, withIntermediateDirectories: true) // silently fails!
|
|
295
|
+
self.dataDirectory = dir
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Verify
|
|
300
|
+
1. Add temporary logging of the resolved path
|
|
301
|
+
2. Verify `contentsOfDirectory` succeeds on the chosen path
|
|
302
|
+
3. Write a test file and read it back
|
|
303
|
+
|
|
304
|
+
### Notes
|
|
305
|
+
- Common in: Debug builds from Xcode without full iCloud provisioning, capability added but container not created in Apple Developer portal, first launch before iCloud sync initialized, user not signed into iCloud
|
|
306
|
+
- In sandboxed macOS apps, `FileManager.urls(for: .documentDirectory)` returns `~/Library/Containers/<bundleId>/Data/Documents/`, not the user's Documents folder
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## 5. iCloud symlink content corruption (sparse / dehydrated files)
|
|
311
|
+
|
|
312
|
+
### Trigger
|
|
313
|
+
- `stat -f '%z' file` shows size > 0 but `stat -f '%b' file` shows `0` blocks
|
|
314
|
+
- Files open and read successfully but return empty content
|
|
315
|
+
- `JSONDecoder` fails on files that "exist" with non-zero reported size
|
|
316
|
+
- Content directory was symlinked or rsynced into iCloud Drive
|
|
317
|
+
|
|
318
|
+
### Root cause
|
|
319
|
+
When you migrate content from local storage to iCloud Drive (symlink or rsync), iCloud may create "dehydrated" placeholder files: non-zero `st_size` but `st_blocks=0`. The files appear to exist but read as empty. This silently corrupts data.
|
|
320
|
+
|
|
321
|
+
### Fix
|
|
322
|
+
1. Detect sparse files (macOS `stat` syntax) — null-delimited so filenames with spaces/newlines are handled correctly:
|
|
323
|
+
```bash
|
|
324
|
+
find /path/to/content -type f -print0 | while IFS= read -r -d '' f; do
|
|
325
|
+
size=$(stat -f '%z' "$f" 2>/dev/null)
|
|
326
|
+
blocks=$(stat -f '%b' "$f" 2>/dev/null)
|
|
327
|
+
if [ "$size" -gt 0 ] 2>/dev/null && [ "$blocks" = "0" ]; then
|
|
328
|
+
echo "SPARSE: $f"
|
|
329
|
+
fi
|
|
330
|
+
done
|
|
331
|
+
```
|
|
332
|
+
2. Recovery priority order: git history (only for previously-tracked files) → Time Machine → manual reconstruction.
|
|
333
|
+
3. Delete irrecoverable sparse placeholders — they waste space and cause misleading `stat` results.
|
|
334
|
+
4. **Never rsync local → iCloud with `--delete`** if you suspect corruption — the backup mirrors the corruption.
|
|
335
|
+
|
|
336
|
+
### Audit checks
|
|
337
|
+
Flag any code that mirrors content into `~/Library/Mobile Documents/` paths without integrity verification (e.g., post-write `stat` or hash check). Binary files (images, video) that were never in git are completely irrecoverable from history.
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## 6. SwiftUI xcstrings localization silent failures
|
|
342
|
+
|
|
343
|
+
A connected family of bugs. Each is silent — the app builds and runs, but UI ships in the wrong language.
|
|
344
|
+
|
|
345
|
+
### 6.1 — `Text(stringVariable)` does NOT auto-localize
|
|
346
|
+
**Trigger:** strings appear in English in the UI; xcstrings has correct translations; device language is set correctly.
|
|
347
|
+
**Cause:** `Text("literal")` accepts `LocalizedStringKey` and auto-localizes. `Text(someVar)` where `someVar: String` does NOT — it renders raw. Any reusable component accepting `title: String` and passing it to `Text(title)` ships untranslated UI.
|
|
348
|
+
**Fix:** Either change the parameter type to `LocalizedStringKey` (for literal-only callers), OR document that callers must pre-localize via `String(localized:)`.
|
|
349
|
+
|
|
350
|
+
### 6.2 — `String(localized:)` ignores SwiftUI environment locale
|
|
351
|
+
**Trigger:** in-app language picker changes formatting (dates/numbers) but not string translations.
|
|
352
|
+
**Cause:** `String(localized:)` uses `Bundle.main.preferredLocalizations`, controlled by `UserDefaults["AppleLanguages"]` — NOT by `.environment(\.locale)`. The SwiftUI environment locale only affects formatting, not catalog lookups.
|
|
353
|
+
**Fix:** Either (a) write to `AppleLanguages` and prompt for restart, OR (b) build an `appLocalized()` helper that passes the user's chosen locale explicitly:
|
|
354
|
+
```swift
|
|
355
|
+
extension Locale {
|
|
356
|
+
static var app: Locale {
|
|
357
|
+
let id = UserDefaults.standard.string(forKey: "com.example.appLocale") ?? ""
|
|
358
|
+
return id.isEmpty ? .current : Locale(identifier: id)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
func appLocalized(_ key: String.LocalizationValue, comment: StaticString = "") -> String {
|
|
363
|
+
String(localized: key, locale: .app, comment: comment)
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
Use `appLocalized()` everywhere instead of `String(localized:)`. Always pass `comment:` so the xcstrings extractor populates context for translators.
|
|
367
|
+
|
|
368
|
+
### 6.3 — AGA `^[...](inflect: true)` requires `LocalizedStringKey`
|
|
369
|
+
**Trigger:** Badge or label shows `^[0 horse](inflect: true)` literally instead of "0 horses".
|
|
370
|
+
**Cause:** Apple's Automatic Grammar Agreement only fires when rendered as `Text(LocalizedStringKey)`. `String(localized: "^[\(count) horse](inflect: true)")` returns a plain `String`, strips the AGA pipeline, and renders the markup literally. AGA `inflect: true` is also unreliable for non-English locales.
|
|
371
|
+
**Fix:** Use xcstrings `variations.plural` for ALL locales including English:
|
|
372
|
+
```json
|
|
373
|
+
"^[%lld horse](inflect: true)": {
|
|
374
|
+
"localizations": {
|
|
375
|
+
"en": {
|
|
376
|
+
"variations": { "plural": {
|
|
377
|
+
"one": { "stringUnit": { "state": "translated", "value": "%lld horse" } },
|
|
378
|
+
"other": { "stringUnit": { "state": "translated", "value": "%lld horses" } }
|
|
379
|
+
}}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### 6.4 — Static cached `DateFormatter` breaks locale switching
|
|
386
|
+
**Trigger:** Dates render in the original device language even after the user switches in-app language.
|
|
387
|
+
**Cause:** Static `DateFormatter` instances capture locale at creation time and never update.
|
|
388
|
+
**Fix:** Use `Date.FormatStyle` (respects current locale, Foundation caches internally), OR construct a fresh formatter per call. Static caching IS safe for locale-independent formats like `yyyyMMdd` — set `locale = Locale(identifier: "en_US_POSIX")` to prevent calendar interference.
|
|
389
|
+
|
|
390
|
+
### 6.5 — `date.formatted(...)` vs `Text(date, format:)` in views
|
|
391
|
+
**Cause:** `date.formatted(...)` returns a `String` using `Locale.current` (system locale, non-reactive). `Text(date, format: ...)` uses the SwiftUI environment locale and IS reactive.
|
|
392
|
+
**Fix:** Prefer `Text(date, format: .dateTime.weekday(.abbreviated))` in view bodies.
|
|
393
|
+
|
|
394
|
+
### 6.6 — Localized strings stored in SwiftData / CoreData
|
|
395
|
+
**Cause:** Storing `"Lektion"` (German) and reading in French gives mixed-language UI.
|
|
396
|
+
**Fix:** Always store raw enum values; compute `displayName` at render time:
|
|
397
|
+
```swift
|
|
398
|
+
@Model final class Transaction {
|
|
399
|
+
var categoryRaw: String = "lesson" // raw enum value
|
|
400
|
+
var category: TransactionCategory {
|
|
401
|
+
get { TransactionCategory(rawValue: categoryRaw) ?? .other }
|
|
402
|
+
set { categoryRaw = newValue.rawValue }
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
Same rule for dates: store `Date`, format for display only. Use `ISO8601DateFormatter` for export/import.
|
|
407
|
+
|
|
408
|
+
### 6.7 — Missing `comment:` parameter on `String(localized:)`
|
|
409
|
+
Translators have no context, and the xcstrings extractor won't auto-populate hints. Always pass `comment:`.
|
|
410
|
+
|
|
411
|
+
### 6.8 — Missing `en` entry in xcstrings when other languages are present
|
|
412
|
+
Causes English UI to display with raw `^[...]` markup or fall back to keys.
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## 7. XcodeGen project generation gotchas
|
|
417
|
+
|
|
418
|
+
### 7.1 — Missing bundle ID on simulator install
|
|
419
|
+
**Trigger:** `xcodebuild test` fails with "Simulator device failed to install the application. Missing bundle ID." despite `PRODUCT_BUNDLE_IDENTIFIER` being correctly set.
|
|
420
|
+
**Cause:** `GENERATE_INFOPLIST_FILE: false` with a custom Info.plist that contains only app-specific keys. Xcode expects the plist to contain ALL required keys (`CFBundleIdentifier`, `CFBundleExecutable`, `CFBundlePackageType`).
|
|
421
|
+
**Fix:** Set `GENERATE_INFOPLIST_FILE: true` even when providing a custom Info.plist. Xcode will merge your custom keys with auto-generated standard keys.
|
|
422
|
+
```yaml
|
|
423
|
+
settings:
|
|
424
|
+
base:
|
|
425
|
+
INFOPLIST_FILE: MyApp/Info.plist
|
|
426
|
+
GENERATE_INFOPLIST_FILE: true # merges custom + standard keys
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### 7.2 — Preview Content Swift files not compiling
|
|
430
|
+
**Trigger:** Build fails with "cannot find 'PreviewSampleData' in scope".
|
|
431
|
+
**Cause:** `buildPhase: none` on the entire `Preview Content/` directory excludes ALL files including Swift sources. Correct for asset catalogs, wrong for Swift files needed at runtime (e.g., `PreviewSampleData.swift` used via `-SeedSampleData` launch argument).
|
|
432
|
+
**Fix:** Only exclude the asset catalog, not the entire directory:
|
|
433
|
+
```yaml
|
|
434
|
+
# CORRECT
|
|
435
|
+
sources:
|
|
436
|
+
- path: MyApp
|
|
437
|
+
excludes:
|
|
438
|
+
- Preview Content/PreviewAssets.xcassets
|
|
439
|
+
- path: MyApp/Preview Content/PreviewAssets.xcassets
|
|
440
|
+
buildPhase: none
|
|
441
|
+
```
|
|
442
|
+
Note: in Release builds on CI, `DEVELOPMENT_ASSET_PATHS` files may be stripped — for runtime-needed Swift files, MOVE them OUT of `Preview Content/` into the main source tree.
|
|
443
|
+
|
|
444
|
+
### 7.3 — UILaunchScreen disappears after `xcodegen generate` (iOS letterbox mode)
|
|
445
|
+
**Trigger:** iOS app renders in a tiny letterboxed/compatibility window with large black borders. App was full-screen before but regressed after `xcodegen generate`.
|
|
446
|
+
**Cause:** When using XcodeGen's `info.path`, `xcodegen generate` **overwrites the entire plist file** from scratch using only the keys in `info.properties`. Any keys you manually added to the plist (like `UILaunchScreen`) are silently deleted. Without `UILaunchScreen` (even as an empty dict), iOS falls back to legacy compatibility mode.
|
|
447
|
+
**Fix:** Move `UILaunchScreen` into `project.yml`'s `info.properties`:
|
|
448
|
+
```yaml
|
|
449
|
+
targets:
|
|
450
|
+
MyApp:
|
|
451
|
+
info:
|
|
452
|
+
path: MyApp/Info.plist
|
|
453
|
+
properties:
|
|
454
|
+
UILaunchScreen: {} # Empty dict = auto-generated launch screen
|
|
455
|
+
CFBundleDisplayName: MyApp
|
|
456
|
+
```
|
|
457
|
+
Also REMOVE `INFOPLIST_KEY_UILaunchScreen_Generation: true` from build settings if both are present — they create a nested `UILaunchScreen > UILaunchScreen` structure. After fixing, **uninstall the app from the simulator** before reinstalling — the simulator caches the old launch screen configuration.
|
|
458
|
+
|
|
459
|
+
### 7.4 — General rule
|
|
460
|
+
**Never manually edit the Info.plist file when using XcodeGen's `info.path`.** All custom keys must go in `info.properties` in `project.yml`, or they will be lost on next `xcodegen generate`.
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## 8. TestFlight upload validation gotchas
|
|
465
|
+
|
|
466
|
+
These all happen SERVER-SIDE at Apple, not during local build/archive. CI shows green and the build succeeds, but the IPA never appears in TestFlight.
|
|
467
|
+
|
|
468
|
+
### 8.1 — iPad multitasking interface orientations (code 409, fatal)
|
|
469
|
+
**Symptom:** `Invalid bundle. The "UIInterfaceOrientationPortrait" orientations were provided ... but you need to include all of the [4 orientations] to support iPad multitasking.`
|
|
470
|
+
**Cause:** Apple requires all 4 interface orientations declared in `UISupportedInterfaceOrientations`, OR the app must opt out of iPad multitasking via `UIRequiresFullScreen: true`. Applies even to iPhone-only apps.
|
|
471
|
+
**Fix:** Add to Info.plist:
|
|
472
|
+
```xml
|
|
473
|
+
<key>UISupportedInterfaceOrientations</key>
|
|
474
|
+
<array>
|
|
475
|
+
<string>UIInterfaceOrientationPortrait</string>
|
|
476
|
+
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
|
477
|
+
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
478
|
+
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
479
|
+
</array>
|
|
480
|
+
<key>UIRequiresFullScreen</key>
|
|
481
|
+
<true/>
|
|
482
|
+
```
|
|
483
|
+
Or as XcodeGen build settings:
|
|
484
|
+
```yaml
|
|
485
|
+
INFOPLIST_KEY_UISupportedInterfaceOrientations: "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"
|
|
486
|
+
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad: "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"
|
|
487
|
+
INFOPLIST_KEY_UIRequiresFullScreen: YES
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### 8.2 — Missing document configuration (code 90737, warning today / fatal future)
|
|
491
|
+
**Symptom:** `Missing Document Configuration. By declaring the CFBundleDocumentTypes key ... you've indicated that your app is able to open documents. Please set the LSSupportsOpeningDocumentsInPlace key to YES (recommended) or NO.`
|
|
492
|
+
**Cause:** Declaring `CFBundleDocumentTypes` requires also declaring how the app handles document access.
|
|
493
|
+
**Fix:**
|
|
494
|
+
```xml
|
|
495
|
+
<key>LSSupportsOpeningDocumentsInPlace</key>
|
|
496
|
+
<true/>
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### 8.3 — `apple-actions/upload-testflight-build` reports success on UPLOAD FAILED
|
|
500
|
+
**Cause:** The action wraps `xcrun altool`, which outputs validation errors in XML plist format. The action exit code can still be 0 even when the plist contains `UPLOAD FAILED`.
|
|
501
|
+
**Fix:** In CI release workflows, always grep raw upload logs:
|
|
502
|
+
```bash
|
|
503
|
+
gh run view --job=<JOB_ID> --log 2>&1 | grep -i -E '(UPLOAD FAILED|product-errors|product-warnings)'
|
|
504
|
+
```
|
|
505
|
+
Always check TestFlight after CI completes to confirm the build actually arrived. Don't trust the green checkmark.
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## 9. xcodebuild App Group provisioning auth failure
|
|
510
|
+
|
|
511
|
+
### Trigger
|
|
512
|
+
- `xcodebuild archive -allowProvisioningUpdates` fails with: `Authentication failed: Make sure a bearer token was provided, it is properly configured and signed, and it has not expired.`
|
|
513
|
+
- AND: `Provisioning profile "iOS Team Provisioning Profile: <bundle-id>" doesn't include the App Groups capability`
|
|
514
|
+
- The same API key WORKS for `xcrun altool --upload-app`
|
|
515
|
+
- Common scenario: adding a WidgetKit extension with shared App Group to an existing app
|
|
516
|
+
|
|
517
|
+
### Root cause
|
|
518
|
+
The App Store Connect API key (`.p8`) has different permission scopes:
|
|
519
|
+
- **Upload / App Management**: works via `altool` for submitting builds
|
|
520
|
+
- **Provisioning Profile Management**: requires Xcode GUI session OR an API key with Admin/Developer role and certificate/profile management permissions
|
|
521
|
+
|
|
522
|
+
When `xcodebuild` encounters a new capability not in the existing provisioning profile, it tries to register the App Group identifier and regenerate profiles via the Apple Developer Portal API. This requires higher permissions than upload-only.
|
|
523
|
+
|
|
524
|
+
The error message ("bearer token not provided") is misleading — it's actually a permissions issue.
|
|
525
|
+
|
|
526
|
+
### Fix
|
|
527
|
+
Manually fix provisioning ONCE in Xcode GUI before committing to CI:
|
|
528
|
+
1. `open YourProject.xcodeproj`
|
|
529
|
+
2. Select the main app target → Signing & Capabilities
|
|
530
|
+
3. Click "Fix Issue" / "Register" for each capability
|
|
531
|
+
4. Repeat for the widget/extension target
|
|
532
|
+
5. Once Xcode builds successfully, the CLI deploy script will work
|
|
533
|
+
|
|
534
|
+
Alternative: register the App Group + bundle ID capabilities manually via the Apple Developer Portal at `https://developer.apple.com/account/resources/identifiers/`.
|
|
535
|
+
|
|
536
|
+
For XcodeGen multi-platform targets with widget extensions: use `platformFilter: iOS` on the dependency since widgets are primarily iOS.
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
## 10. iOS first-IAP submission rejection (Guideline 2.1b / 3.1.1)
|
|
541
|
+
|
|
542
|
+
### Trigger
|
|
543
|
+
- App rejected with "Unable to make IAP purchases" in sandbox review
|
|
544
|
+
- All IAPs show "Developer Action Needed" / "Rejected" status
|
|
545
|
+
- Error in app: "Purchase failed: Unable to Complete Request"
|
|
546
|
+
- App rejected for missing "Restore Purchases" button (Guideline 3.1.1)
|
|
547
|
+
|
|
548
|
+
### Root causes (often combined)
|
|
549
|
+
1. **Missing Restore button:** apps that offer IAPs MUST include a distinct, user-tappable Restore button on the same screen where IAPs are shown. Not buried in deep settings.
|
|
550
|
+
2. **Hardcoded fallback price:** `Text(price ?? "$0.99")` shows a tappable button before products load, which the App Reviewer taps and gets a failed purchase.
|
|
551
|
+
3. **First-time IAPs CANNOT be tested in TestFlight sandbox.** Products will load (prices display correctly) but `product.purchase()` will fail with "Unable to Complete Request". This is NOT a code bug — it's a sandbox limitation for unapproved IAPs.
|
|
552
|
+
|
|
553
|
+
### Fixes
|
|
554
|
+
|
|
555
|
+
**Restore button (StoreKit 2):**
|
|
556
|
+
```swift
|
|
557
|
+
func restorePurchases() async -> Bool {
|
|
558
|
+
do {
|
|
559
|
+
try await AppStore.sync()
|
|
560
|
+
} catch {
|
|
561
|
+
purchaseError = "Failed to restore: \(error.localizedDescription)"
|
|
562
|
+
return false
|
|
563
|
+
}
|
|
564
|
+
await refreshPurchaseState()
|
|
565
|
+
return true
|
|
566
|
+
}
|
|
567
|
+
```
|
|
568
|
+
Place the button on the same screen where IAPs are shown AND in Settings for discoverability. Show loading state during restore, error/success alerts.
|
|
569
|
+
|
|
570
|
+
**PurchaseButton UX — never show a hardcoded fallback price:**
|
|
571
|
+
```swift
|
|
572
|
+
// BAD: shows tappable $0.99 even when product isn't loaded
|
|
573
|
+
Text(price ?? "$0.99")
|
|
574
|
+
|
|
575
|
+
// GOOD: ProgressView until product resolves
|
|
576
|
+
if let price {
|
|
577
|
+
Button(action: action) { Text(price) }
|
|
578
|
+
} else {
|
|
579
|
+
ProgressView()
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
**Local StoreKit testing:** add a `.storekit` configuration file and reference it in the scheme's `LaunchAction` (`storeKitConfigurationFileReference`). Run from Xcode — purchases use the local test environment with no App Store Connect dependency. Note: only affects debug runs; archived/TestFlight builds use real sandbox.
|
|
584
|
+
|
|
585
|
+
**Submitting first-time IAPs alongside the app version (the App Store Connect UI changed):**
|
|
586
|
+
1. Go to the app version page → click "Add for Review" — this creates a Draft Submission
|
|
587
|
+
2. Go to each individual IAP page → click "Submit for Review" on each — adds it to the same draft
|
|
588
|
+
3. Open the Draft Submission (bottom of any page) → click "Submit for Review"
|
|
589
|
+
|
|
590
|
+
**Clearing rejected IAP localizations:** for each IAP, edit the English (U.S.) localization (any minor edit), Save → status changes from "Rejected" to "Prepare for Submission". This can be automated with Playwright since App Store Connect is a web app.
|
|
591
|
+
|
|
592
|
+
### Notes
|
|
593
|
+
- Sandbox tester accounts (Users and Access > Sandbox) now require an existing Apple Account
|
|
594
|
+
- For TestFlight, sandbox purchases use the user's real Apple ID automatically — no separate sandbox account needed
|
|
595
|
+
- Bank account "Processing" status does NOT block sandbox purchases for approved IAPs
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
599
|
+
## 11. `.foregroundStyle(.accentColor)` compile failure
|
|
600
|
+
|
|
601
|
+
### Trigger
|
|
602
|
+
SwiftUI compile error on `.foregroundStyle(.accentColor)`.
|
|
603
|
+
|
|
604
|
+
### Cause
|
|
605
|
+
`ShapeStyle` has no `.accentColor` member.
|
|
606
|
+
|
|
607
|
+
### Fix
|
|
608
|
+
```swift
|
|
609
|
+
// WRONG
|
|
610
|
+
.foregroundStyle(.accentColor)
|
|
611
|
+
|
|
612
|
+
// CORRECT
|
|
613
|
+
.foregroundStyle(Color.accentColor)
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
---
|
|
617
|
+
|
|
618
|
+
## 12. Keychain test failures in simulator (CryptoKit)
|
|
619
|
+
|
|
620
|
+
### Trigger
|
|
621
|
+
- AES-GCM encryption with key stored in Keychain via CryptoKit
|
|
622
|
+
- `SecItemAdd` and `SecItemCopyMatching` silently return non-success in test environment
|
|
623
|
+
- Encrypt-then-decrypt roundtrip test fails because a new key is generated on each `getOrCreateKey()` call
|
|
624
|
+
|
|
625
|
+
### Fix
|
|
626
|
+
Add an in-memory key cache as fallback so tests don't depend on Keychain persistence:
|
|
627
|
+
```swift
|
|
628
|
+
private static var cachedKey: SymmetricKey?
|
|
629
|
+
|
|
630
|
+
private static func getOrCreateKey() -> SymmetricKey? {
|
|
631
|
+
if let existingKey = loadKeyFromKeychain() {
|
|
632
|
+
cachedKey = existingKey
|
|
633
|
+
return existingKey
|
|
634
|
+
}
|
|
635
|
+
if let cached = cachedKey {
|
|
636
|
+
return cached
|
|
637
|
+
}
|
|
638
|
+
let newKey = SymmetricKey(size: .bits256)
|
|
639
|
+
cachedKey = newKey
|
|
640
|
+
saveKeyToKeychain(newKey)
|
|
641
|
+
return newKey
|
|
642
|
+
}
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
## How audit agents should use this catalogue
|
|
648
|
+
|
|
649
|
+
1. In Phase 0, the orchestrator detects project characteristics (CloudKit, SwiftData, iCloud, xcstrings, StoreKit, XcodeGen, CI release path) and lists the relevant entries from this catalogue's quick index for each downstream audit agent.
|
|
650
|
+
2. Audit agents grep / read for the **TRIGGER** signals in each relevant entry. When a trigger matches, the agent files a finding referencing the entry number and severity.
|
|
651
|
+
3. Remediation agents receive this catalogue as part of their context. When fixing an issue that matches an entry, they apply the FIX **as written** rather than improvising. The fixes here have all shipped in real projects.
|
|
652
|
+
4. Test enhancement (Phase 4c) uses the test patterns embedded in entries 1, 2, 6, and 10 (CloudKit smoke test, `testModelContainerSchemaIsValid`, localization round-trip, IAP product-loading test) when the relevant project characteristics are present.
|
|
653
|
+
|
|
654
|
+
Severity guidance:
|
|
655
|
+
- **CRITICAL**: app-launch crashes (#1, #2), App Store rejection (#10 missing Restore), data loss (#5)
|
|
656
|
+
- **HIGH**: silent data corruption (#4, #6.6), build/release pipeline failures (#7.1, #7.3, #8.1), compile failures (#11)
|
|
657
|
+
- **MEDIUM**: UX bugs that ship to users (#6.1–6.5, #6.7), warnings that may become fatal (#8.2)
|
|
658
|
+
- **LOW**: code clarity / convention drift
|
package/package.json
CHANGED
package/uninstall.sh
CHANGED
|
@@ -32,7 +32,7 @@ OLD_COMMANDS=(cam good makegoals makegood optimize-md)
|
|
|
32
32
|
|
|
33
33
|
LIBS=(
|
|
34
34
|
code-review-checklist copilot-review-loop graphql-escaping
|
|
35
|
-
remediation-agent-template swift-review-checklist
|
|
35
|
+
remediation-agent-template swift-review-checklist swift-gotchas
|
|
36
36
|
review-surface-scan review-security-audit review-cross-file-tracing
|
|
37
37
|
)
|
|
38
38
|
|