1190 lines
124 KiB
Markdown
1190 lines
124 KiB
Markdown
# Hardening Audit Report
|
||
|
||
## Audit Sources
|
||
- 11 mapper agents (100% file coverage)
|
||
- 18 specialized domain auditors (parallel)
|
||
- 1 cross-cutting deep audit (parallel)
|
||
- Total source files: 163
|
||
|
||
---
|
||
|
||
## CRITICAL — Will crash or lose data
|
||
|
||
## BUG — Incorrect behavior
|
||
|
||
## SILENT FAILURE — Error swallowed or ignored
|
||
|
||
## RACE CONDITION — Concurrency issue
|
||
|
||
## LOGIC ERROR — Code doesn't match intent
|
||
|
||
## PERFORMANCE — Unnecessary cost
|
||
|
||
## ACCESSIBILITY — Usability barrier
|
||
|
||
## SECURITY — Vulnerability or exposure
|
||
|
||
## MODERNIZATION — Legacy pattern to update
|
||
|
||
## DEAD CODE / UNREACHABLE
|
||
|
||
## FRAGILE — Works now but will break easily
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
---
|
||
## SOURCE: Memory Auditor (22 findings)
|
||
|
||
**`Shared/Services/ImageCache.swift`:24** | CRITICAL | NotificationCenter observer token leaked — never stored or removed
|
||
- What: `NotificationCenter.default.addObserver(forName:object:queue:using:)` is called in `init()`. The returned opaque observer token is discarded entirely with no variable. The block-based API requires the token to be stored and passed to `removeObserver(_:)` in `deinit`.
|
||
- Impact: Each `ImageCache` initialization registers an observer that is never deregistered. In extension contexts where `ImageCache` might be initialized more than once, each initialization accumulates another dangling registration that fires `clearCache()` on every memory warning.
|
||
|
||
**`Shared/Persisence/DataController.swift`:51-53** | CRITICAL | `editedDataClosure` array grows forever — no removal mechanism
|
||
- What: `addNewDataListener(closure:)` appends closures to `editedDataClosure` with no paired remove function. Every call site permanently stores its closure inside the singleton `DataController.shared`. If those closures capture `self` (the view model), the view model is kept alive by the singleton indefinitely.
|
||
- Impact: View models and any objects they capture are never deallocated. Each navigation to a screen that calls `addNewDataListener` leaks one closure and its retained objects. `refreshFromDisk()` iterates this ever-growing array and calls every closure including ones from long-dead view models.
|
||
|
||
**`Shared/Views/EntryListView.swift`:2224** | CRITICAL | `MotionManager` accelerometer never stopped — no `stop()` call in any lifecycle hook
|
||
- What: `MotionCardView.body` calls `motionManager.startIfNeeded()` in `.onAppear` (starts CMMotionManager at 30 Hz), but there is no `.onDisappear` modifier calling `motionManager.stop()`. The `stop()` function exists but is never invoked from any view.
|
||
- Impact: Accelerometer runs at 30 Hz continuously for the entire app session once any `MotionCardView` appears. Constant battery drain regardless of whether the motion-style list is visible.
|
||
|
||
**`Shared/Views/SettingsView/LiveActivityPreviewView.swift`:325-356** | CRITICAL | `ImageRenderer` called on background thread — threading violation
|
||
- What: `DispatchQueue.global(qos: .userInitiated).async` at line 325 calls `ImageRenderer(content:).uiImage` on a background thread. `ImageRenderer` is `@MainActor` and must be used on the main thread only.
|
||
- Impact: Undefined behavior — crashes, blank images, or data corruption. Direct violation of Apple's concurrency contract.
|
||
|
||
**`Shared/Services/WatchConnectivityManager.swift`:117-124** | CRITICAL | `pendingMoods` array accessed from multiple threads — data race
|
||
- What: `WatchConnectivityManager` has no actor isolation. `pendingMoods` is appended in `sendMoodToPhone` (called from any context) and read+cleared in `activationDidCompleteWith` (called on a WCSession background queue). No lock, serial queue, or actor isolation protects concurrent access.
|
||
- Impact: Data race on `pendingMoods` can corrupt the array — crashes, lost pending moods, or duplicate sends.
|
||
|
||
**`Shared/MoodStreakActivity.swift`:215-222** | CRITICAL | Infinite loop risk in `getStreakData()` when `calendar.date(byAdding:)` returns nil
|
||
- What: `while true` loop advances `checkDate` via `calendar.date(byAdding: .day, value: -1, to: checkDate) ?? checkDate`. If `date(byAdding:)` returns `nil`, `checkDate` does not advance and the loop never breaks.
|
||
- Impact: Infinite loop hangs the main actor indefinitely, freezing the app or widget process.
|
||
|
||
**`Shared/Views/CelebrationAnimations.swift`:109** | BUG | `DispatchQueue.main.asyncAfter` holding `onComplete` closure fires after view dismissal — no cancellation
|
||
- What: `onComplete()` (saves mood entry to Core Data) is scheduled via `asyncAfter`. If the user navigates away before the animation duration expires, the callback fires anyway, saving a mood entry.
|
||
- Impact: Ghost entries can be saved to Core Data after the user has navigated away. No cancellation mechanism exists.
|
||
|
||
**`Shared/DemoAnimationManager.swift`:60-70** | BUG | Orphaned `DispatchQueue.main.asyncAfter` closures when `restartAnimation()` called rapidly
|
||
- What: Both `startDemoMode()` and `restartAnimation()` schedule `asyncAfter` with 3-second delay to call `beginAnimation()`. `restartAnimation()` invalidates the Timer but cannot cancel the GCD block. Rapid calls queue multiple `beginAnimation()` invocations.
|
||
- Impact: Multiple `Timer` instances run simultaneously at 60 Hz, each mutating `animationProgress`, causing corrupt animation state and battery drain.
|
||
|
||
**`Shared/DemoAnimationManager.swift`:91-95** | BUG | Timer fires at 60 Hz into `Task { @MainActor }`, accumulating tasks under load
|
||
- What: Timer callback enqueues `Task { @MainActor in self?.updateProgress() }` on every tick (60/sec). If the main thread is busy, tasks pile up causing `animationProgress` to jump discontinuously.
|
||
- Impact: Animation stutters. Up to 60 `Task` objects per second simultaneously alive, each holding a strong reference to `self`.
|
||
|
||
**`Shared/FeelsApp.swift`:92** | BUG | `Task.detached(priority: .utility) { @MainActor in ... }` runs everything on main thread
|
||
- What: Three heavy DataController operations (`refreshFromDisk`, `removeDuplicates`, `fillInMissingDates`) plus analytics run in a `Task.detached` tagged `@MainActor`. The `@MainActor` annotation forces entire body back onto the main thread — `detached` provides zero off-main-thread benefit.
|
||
- Impact: Every foreground transition blocks the main thread with Core Data work, causing UI unresponsiveness for users with large datasets.
|
||
|
||
**`Shared/FeelsApp.swift`:110-117** | WARNING | Fire-and-forget `Task.detached` blocks pile up on rapid scene transitions
|
||
- What: Multiple `Task.detached` closures for `scheduleBasedOnCurrentTime()` and `processPendingSideEffects()` have no cancellation handle. Rapid foreground transitions queue stacks of these tasks concurrently, each mutating shared state.
|
||
- Impact: Under rapid scene transitions, multiple concurrent mutations to subscription status, Live Activity scheduling, and pending side effects.
|
||
|
||
**`Shared/Views/InsightsView/InsightsViewModel.swift`:88-113** | PERFORMANCE | `withTaskGroup` child tasks all `@MainActor` — run serially, negating concurrency
|
||
- What: All three child tasks in `generateAllInsights()` are `@MainActor`-isolated. The main actor is a serial executor, so the tasks run sequentially rather than in parallel.
|
||
- Impact: Three AI inference calls run in series on the main actor, blocking UI for their full combined duration.
|
||
|
||
**`Shared/MoodStreakActivity.swift`:306** | BUG | `scheduleEnd` Timer has no `[weak self]` — inconsistent with `scheduleStart`
|
||
- What: `scheduleEnd(at:)` Timer callback captures singletons directly without `[weak self]`. Inconsistent with `scheduleStart` which uses `[weak self]`. If refactored away from singletons, this becomes a retain cycle.
|
||
- Impact: Low current risk (singletons), but fragile and inconsistent capture semantics.
|
||
|
||
**`Shared/IAPManager.swift`:465-474** | WARNING | `deinit` cancel of `listenForTransactions()` Task is dead code — singleton never deinits
|
||
- What: Task is cancelled in `deinit`, but `IAPManager` is a singleton and `deinit` is never called in production. The `[weak self]` and `deinit` cancel are dead code creating a misleading code contract.
|
||
- Impact: If `IAPManager` is ever instantiated non-singleton (e.g., in tests), the task continues iterating `Transaction.updates` after deallocation, silently calling methods on nil self.
|
||
|
||
**`Shared/Views/InsightsView/InsightsViewModel.swift`:60-63** | WARNING | `generateInsights()` spawns unstructured `Task{}` with no stored handle — multiple concurrent tasks possible
|
||
- What: `generateInsights()` creates `Task{}` without storing the handle. Multiple `.onAppear` or pull-to-refresh calls can spawn concurrent tasks writing to the same `@Published` arrays.
|
||
- Impact: Flickering loading states; potential for two in-flight `LanguageModelSession` instances consuming memory.
|
||
|
||
---
|
||
## SOURCE: IAP Auditor (16 findings)
|
||
|
||
**`Shared/IAPManager.swift`:274** | CRITICAL | `Transaction.currentEntitlements` never calls `transaction.finish()`
|
||
- What: The loop iterating `currentEntitlements` processes transactions but never calls `await transaction.finish()`. StoreKit 2 requires `finish()` to be called after processing every transaction.
|
||
- Impact: Transactions remain unfinished in the payment queue indefinitely, re-delivered on subsequent app launches causing repeated processing.
|
||
|
||
**`Shared/IAPManager.swift`:289** | CRITICAL | `try?` on `subscription.status` silently grants subscribed state on StoreKit errors
|
||
- What: `let statuses = try? await subscription.status` discards StoreKit errors. If the call throws (network offline, StoreKit unavailable), `statuses` is `nil` and execution falls through to line 333 which grants `.subscribed` as a fallback.
|
||
- Impact: A user with expired/cancelled subscription gets full premium access whenever StoreKit is temporarily unavailable. Revenue-loss vulnerability.
|
||
|
||
**`Shared/IAPManager.swift`:333** | CRITICAL | Fallback unconditionally grants `.subscribed` when status retrieval fails
|
||
- What: Line 333 grants `state = .subscribed(...)` whenever `product.subscription` is nil or `subscription.status` fails. Any condition preventing subscription type retrieval silently upgrades the user.
|
||
- Impact: StoreKit failures grant premium access with no actual entitlement verification.
|
||
|
||
**`Shared/IAPManager.swift`:193-202** | CRITICAL | Revoked/expired subscriptions restore `.subscribed` on next cold launch via cache
|
||
- What: Terminal states (`.expired`, `.revoked`) are handled but do NOT clear the cached expiration date. On the next cold launch, `checkForActiveSubscription` returns false, the terminal guard is skipped (state was reset), and the cache fallback (line 207) grants `.subscribed` again.
|
||
- Impact: Revocation does not survive a cold relaunch. Users whose subscriptions are revoked (e.g., refund) get premium access back on next launch.
|
||
|
||
**`Shared/IAPManager.swift`:207-211** | BUG | Cache fallback restores `.subscribed` even when StoreKit explicitly found no active subscription
|
||
- What: After a live StoreKit check found no active subscription, lines 206-211 restore `.subscribed` from cache if the cached expiration is in the future. The cache is never cleared when StoreKit explicitly returns expired/revoked.
|
||
- Impact: Users who cancel subscriptions retain full access until cached expiration date, regardless of StoreKit's live result.
|
||
|
||
**`Shared/Views/FeelsSubscriptionStoreView.swift`:51** | BUG | `dismiss()` called outside async Task — executes before `checkSubscriptionStatus()` completes
|
||
- What: `dismiss()` is called synchronously immediately after spawning `Task { @MainActor in ... }`, not inside it. The view dismisses before subscription status is confirmed and analytics complete.
|
||
- Impact: UI remains in stale IAP state after purchase; async task continues running against a dismissed view's environment.
|
||
|
||
**`Shared/Onboarding/views/OnboardingSubscription.swift`:139-142** | BUG | `onDismiss` unconditionally completes onboarding regardless of purchase outcome
|
||
- What: `.sheet(isPresented:onDismiss:)` calls `completionClosure(onboardingData)` in `onDismiss` whether the user subscribed or cancelled the sheet.
|
||
- Impact: Onboarding completes even when user dismisses paywall without subscribing. Also fires `onboardingCompleted` analytics twice if user used the skip path first.
|
||
|
||
**`Shared/IAPManager.swift`:465-473** | WARNING | `Transaction.updates` finishes transactions but `currentEntitlements` does not — asymmetric
|
||
- What: `listenForTransactions()` correctly calls `transaction.finish()` on all `Transaction.updates`. But the initial check in `checkForActiveSubscription` never finishes transactions from `currentEntitlements`.
|
||
- Impact: Transactions delivered at launch via `currentEntitlements` will be re-delivered via `Transaction.updates`, causing double-processing.
|
||
|
||
**`Shared/Views/FeelsSubscriptionStoreView.swift`:54-55** | WARNING | `.pending` purchase state tracked as failure in analytics
|
||
- What: `case .success(.pending)` calls `trackPurchaseFailed(error: "pending")`. Pending transactions (Ask to Buy, deferred payment) are valid non-failure states.
|
||
- Impact: Purchase funnel metrics miscounted; user receives no pending-approval UI confirmation.
|
||
|
||
**`Shared/IAPManager.swift`:232-244** | WARNING | `restoreCachedSubscriptionState()` grants `.subscribed` with no expiration from a UserDefaults boolean alone
|
||
- What: When `hasActiveSubscription` is `true` but no expiration date is stored, grants `state = .subscribed(expirationDate: nil, willAutoRenew: false)`. Relies solely on a UserDefaults boolean.
|
||
- Impact: If UserDefaults is corrupted or not cleaned up after expiration on older app versions, users get unlimited premium access until async check completes. Brief window at cold launch.
|
||
|
||
**`Shared/IAPManager.swift`:40-44** | WARNING | `bypassSubscription` debug flag identical in both `#if DEBUG` and `#else` blocks — dead code
|
||
- What: Both branches set `bypassSubscription = false`. The `#if DEBUG` variant is dead code with no difference from release build.
|
||
- Impact: Developers believe a debug bypass exists but it doesn't; risk of accidentally modifying release value during testing.
|
||
|
||
**`Shared/Views/CustomizeView/CustomizeView.swift`:693-699** | WARNING | `openSubscriptionManagement()` failure only `print()`-logged — no user feedback
|
||
- What: `AppStore.showManageSubscriptions(in: windowScene)` failure in `catch` block only calls `print(...)`. No alert or user notification on failure.
|
||
- Impact: Users tapping "Manage Subscription" see nothing happen and receive no explanation if it fails.
|
||
|
||
**`Shared/IAPManager.swift`:156-162** | WARNING | Throttle skips re-check even in `.expired` or `.revoked` state
|
||
- What: 5-minute throttle prevents `checkSubscriptionStatus()` from re-running even when state is `.expired` or `.revoked`. `restore()` also goes through this throttle.
|
||
- Impact: User who restores purchases within 5-minute window of previous check sees unchanged state until throttle expires.
|
||
|
||
**`Shared/FeelsApp.swift`:120** | WARNING | `Task.detached(priority: .background)` calling `@MainActor` subscription check runs on main thread anyway
|
||
- What: `checkSubscriptionStatus()` is `@MainActor`-isolated. `Task.detached(priority: .background)` immediately hops to the main actor, providing no threading benefit.
|
||
- Impact: Network StoreKit call and state mutation occur on main thread during foreground transitions.
|
||
|
||
**`Shared/Views/PurchaseButtonView.swift`:127** | WARNING | "Payment Issue" billing label hardcoded English — not localized
|
||
- What: `Text("Payment Issue")` is a raw string literal while other strings in this file use `String(localized:)`. Shown to users in billing trouble.
|
||
- Impact: Non-English users see "Payment Issue" in English during a critical billing communication.
|
||
|
||
**`Shared/IAPManager.swift`:139-146** | WARNING | `init()` Task has no stored cancellation handle — asymmetric cleanup with `updateListenerTask`
|
||
- What: `Task { await checkSubscriptionStatus() }` in `init()` has no handle stored. `updateListenerTask` is separately cancelled in `deinit`, but the init task cannot be cancelled.
|
||
- Impact: Inconsistent cleanup semantics; in non-singleton usage (tests), initial check task cannot be cancelled.
|
||
|
||
---
|
||
## SOURCE: Concurrency Auditor (33 findings)
|
||
|
||
**`Shared/Services/WatchConnectivityManager.swift`:27** | CRITICAL | Data race on `pendingMoods` array — no actor isolation
|
||
- What: `pendingMoods` is a plain `var` array on a class with no actor annotation. It is written in `sendMoodToPhone` (called from watchOS app context) and both read and written in `activationDidCompleteWith` (called on the WCSession delegate callback queue, a background thread).
|
||
- Impact: Concurrent read/write of the array constitutes a data race. Under Swift 6 strict concurrency this is a compiler error. In practice this can corrupt the array or produce torn reads.
|
||
|
||
**`Shared/Services/WatchConnectivityManager.swift`:93-95** | BUG | `transferUserInfo` called from WCSession error callback on unspecified background thread
|
||
- What: The `sendMessage` error handler closure at line 93 calls `session.transferUserInfo(message)` directly. WCSession error callbacks run on an arbitrary background thread. `WatchConnectivityManager` has no actor isolation.
|
||
- Impact: Potential thread-safety violation calling WCSession API from an indeterminate thread; also silently continues after failure with no reporting to the caller.
|
||
|
||
**`Shared/FeelsApp.swift`:92** | CRITICAL | `Task.detached { @MainActor in }` defeats the purpose of `detached`
|
||
- What: `Task.detached(priority: .utility) { @MainActor in ... }` creates a detached task but immediately re-isolates the body to `@MainActor`. The three Core Data calls — `refreshFromDisk()`, `removeDuplicates()`, `fillInMissingDates()` — all execute synchronously on the main actor.
|
||
- Impact: Heavy Core Data work blocks the main thread on every foreground transition, causing UI jank. The `detached` keyword provides false assurance that this work is off-thread.
|
||
|
||
**`Shared/BGTask.swift`:25** | CRITICAL | `@MainActor` background task calls async function without `await` — task completes before work finishes
|
||
- What: `runFillInMissingDatesTask` is marked `@MainActor` and calls `MoodLogger.shared.processPendingSideEffects()` at line 25 without `await`. Calling it without `await` means it begins execution but the caller does not wait for it to finish.
|
||
- Impact: `task.setTaskCompleted(success: true)` fires before `processPendingSideEffects()` finishes, incorrectly signaling completion to the OS. Background task work may be partially executed.
|
||
|
||
**`Shared/BGTask.swift`:14-28** | CRITICAL | `@MainActor` background task blocks main thread with synchronous Core Data
|
||
- What: `runFillInMissingDatesTask` is annotated `@MainActor`, meaning all its work runs on the main thread. `DataController.shared.fillInMissingDates()` is a synchronous Core Data operation that can scan the entire entries table.
|
||
- Impact: Running `fillInMissingDates()` on the main thread during a background task blocks the UI thread if the app is active, causing ANR-like behavior.
|
||
|
||
**`Shared/BGTask.swift`:36** | BUG | Force unwrap of `Calendar` date result in background scheduler
|
||
- What: `runDate = Calendar.current.date(bySettingHour: 0, minute: 1, second: 0, of: runDate!)` — force-unwraps the previous `date(byAdding:)` result. On DST transitions or calendar edge cases, `date(byAdding:)` can return nil.
|
||
- Impact: Crash in the background task scheduler. Background processing permanently stops scheduling.
|
||
|
||
**`Shared/BGTask.swift`:42** | BUG | String interpolation syntax error silently suppresses error logging
|
||
- What: `print("Could not schedule image fetch: (error)")` uses `(error)` instead of `\(error)`. This is valid Swift — it prints the literal string `"(error)"` rather than interpolating the actual error value.
|
||
- Impact: All `BGTaskScheduler.submit` failures are permanently invisible.
|
||
|
||
**`Shared/Models/UserDefaultsStore.swift`:217** | CRITICAL | Static mutable cache with no synchronization — data race
|
||
- What: `private static var cachedOnboardingData: OnboardingData?` is a static mutable variable with no actor isolation and no locking. Read and written from `@MainActor` contexts and background tasks.
|
||
- Impact: Concurrent reads/writes to this static var constitute a Swift 6 data race. The cache could be read in a partially-written state, returning nil or stale `OnboardingData`.
|
||
|
||
**`Shared/Random.swift`:174** | CRITICAL | Static mutable dictionary `textToImageCache` with no thread safety
|
||
- What: `private static var textToImageCache = [String: UIImage]()` is accessed from `textToImage()` without any synchronization. Widget rendering, background tasks, and main-thread rendering all can call this simultaneously.
|
||
- Impact: Concurrent dictionary read/write is a data race. Under Swift 6 this is a compiler error. In practice it can corrupt the dictionary, producing crashes or wrong images.
|
||
|
||
**`Shared/Random.swift`:48,68** | CRITICAL | Static mutable caches `existingWeekdayName` and `existingDayFormat` with no thread safety
|
||
- What: Two `static var` dictionaries are read/written from `weekdayName(fromDate:)` and `dayFormat(fromDate:)` with no actor isolation or synchronization. Called from widget rendering (background), Core Data background operations, and main thread UI.
|
||
- Impact: Concurrent dictionary mutations are data races.
|
||
|
||
**`Shared/DemoAnimationManager.swift`:60-71** | BUG | Uncancellable `DispatchQueue.main.asyncAfter` causes stacked timer creation
|
||
- What: `restartAnimation()` calls `animationTimer?.invalidate()` then `DispatchQueue.main.asyncAfter(deadline: .now() + startDelay) { self.beginAnimation() }`. The GCD block cannot be cancelled. Multiple timers created and run simultaneously.
|
||
- Impact: Multiple concurrent 60fps timers driving the same `animationProgress` state cause undefined animation behavior and CPU waste.
|
||
|
||
**`Shared/MoodStreakActivity.swift`:215-223** | CRITICAL | Infinite loop when `Calendar.date(byAdding:)` returns nil
|
||
- What: `while true` loop with `?? checkDate` fallback — if `date(byAdding: .day, value: -1, to: checkDate)` returns nil, `checkDate` never changes and the loop never terminates.
|
||
- Impact: Infinite loop on `@MainActor` permanently freezes the UI.
|
||
|
||
**`Shared/Persisence/DataControllerGET.swift`:74-82** | CRITICAL | Infinite loop when `date(byAdding:)` returns nil in `calculateStreak`
|
||
- What: Same `while true` with `?? checkDate` fallback in `calculateStreak(from:)`. On DST edge case, `checkDate` doesn't advance.
|
||
- Impact: Main thread freeze on `@MainActor DataController`. Watchdog terminates the app.
|
||
|
||
**`Shared/AppShortcuts.swift`:128-140** | CRITICAL | Unbounded `while true` loop on `@MainActor` with Core Data fetch per iteration
|
||
- What: `calculateStreak()` runs a `while true` loop on `@MainActor`, fetching a Core Data entry for each past day. For a user with a 3-year streak, this performs 1,095 sequential Core Data round-trips synchronously on the main thread.
|
||
- Impact: Main thread blocked for potentially hundreds of milliseconds to seconds. App appears frozen while Siri shortcut resolves.
|
||
|
||
**`Shared/DataController.swift`:15** | BUG | `@MainActor` singleton may be initialized off-actor on first access
|
||
- What: `static let shared = DataController()` on a `@MainActor final class`. Swift lazily initializes static properties on first access. If accessed before the main actor is running (e.g., from a widget extension or background thread), the `@MainActor` initializer is called off-actor.
|
||
- Impact: SwiftData `ModelContainer` and `ModelContext` created on a non-main thread — undefined behavior.
|
||
|
||
**`Shared/Views/InsightsView/InsightsViewModel.swift`:88-113** | PERFORMANCE | `withTaskGroup` child tasks all `@MainActor` — no real concurrency
|
||
- What: All three `group.addTask { @MainActor in ... }` closures are isolated to `@MainActor`. They execute serially despite using a task group.
|
||
- Impact: Three sequential AI insight generation calls on the main actor. Each can take seconds; UI is blocked waiting for all three.
|
||
|
||
**`Shared/Views/InsightsView/InsightsViewModel.swift`:60-63** | BUG | Concurrent `generateInsights()` calls not guarded — interleaved `@Published` mutations
|
||
- What: `generateInsights()` launches `Task { await generateAllInsights() }` with no guard. Multiple concurrent tasks independently overwrite `@Published` arrays.
|
||
- Impact: Race between two tasks writing the same `@Published` property. Final displayed insights are non-deterministic.
|
||
|
||
**`Shared/Analytics.swift`:614-618** | WARNING | Swift 6 violation — calling `@MainActor` method from non-isolated `ViewModifier`
|
||
- What: `ScreenTrackingModifier.body(content:)` calls `AnalyticsManager.shared.trackScreen(...)` inside `.onAppear`. Under Swift 6, calling a `@MainActor` function from a non-isolated synchronous context is a compile error.
|
||
- Impact: Swift 6 concurrency violation. Will be a hard error once strict concurrency is enforced.
|
||
|
||
**`Shared/Services/PhotoManager.swift`:104-134** | PERFORMANCE | Synchronous disk I/O on `@MainActor`-bound class
|
||
- What: `loadPhoto(id:)` and `loadThumbnail(id:)` call `Data(contentsOf: fullURL)` — synchronous file I/O — on the main actor.
|
||
- Impact: Reading large JPEG files from disk on the main thread causes dropped frames and UI jank during list scrolling.
|
||
|
||
**`Shared/Views/SettingsView/LiveActivityPreviewView.swift`:325-356** | CRITICAL | `ImageRenderer` used on background `DispatchQueue.global` thread
|
||
- What: `DispatchQueue.global(qos: .userInitiated).async { let renderer = ImageRenderer(...); renderer.uiImage }`. `ImageRenderer` is `@MainActor`-isolated per Apple documentation.
|
||
- Impact: Swift 6 data race. Produces blank images, memory corruption, or crashes.
|
||
|
||
**`Shared/Services/SharedMoodIntent.swift`:93-121** | CRITICAL | New `ModelContainer` created per widget intent invocation against shared SQLite store
|
||
- What: `WidgetMoodSaver.save(mood:date:)` creates a fresh `ModelContainer` on every call. Multiple `ModelContainer` writers to the same SQLite file can produce locked errors or data corruption. The delete-then-insert sequence can permanently lose user data if the second save fails.
|
||
- Impact: Data corruption or permanent mood entry loss on concurrent intent + main app writes.
|
||
|
||
**`Shared/Views/SharingTemplates/LongestStreakTemplate.swift`:46-53** | CRITICAL | Non-isolated `init` directly calls `@MainActor`-annotated `configureData()`
|
||
- What: `LongestStreakTemplate.init` is not `@MainActor`-isolated but calls `self.configureData(fakeData:mood:)` which is `@MainActor`. Calling a `@MainActor`-isolated function synchronously from a non-isolated context is a Swift 6 concurrency violation.
|
||
- Impact: Swift 6 will produce a compile error. The data load also silently has no effect since it writes to `@State` vars at init time.
|
||
|
||
**`Shared/Views/Photos/PhotoPickerView.swift`:151** | BUG | `@State` mutation from non-`@MainActor` async context
|
||
- What: `loadImage(from:)` is declared `private func ... async` with no `@MainActor` annotation. Inside it, `isProcessing = true` mutates a `@State` property off the main actor.
|
||
- Impact: Swift 6 data race: writing `@State` off the main actor produces undefined SwiftUI behavior.
|
||
|
||
**`Shared/Views/SettingsView/SettingsView.swift`:831-843,912-941** | BUG | `Task {}` inside `Binding.set` closure mutates `@EnvironmentObject` without `@MainActor`
|
||
- What: `Task { }` closures inside `Binding` set closures mutate `@Published` properties of `@EnvironmentObject` ObservableObjects without explicit `@MainActor` annotation.
|
||
- Impact: Under Swift 6, setting `@Published` properties of `ObservableObject` on a non-main actor is a data race.
|
||
|
||
**`Shared/FeelsApp.swift`:30** | BUG | Force cast of `BGTask` in scheduler callback — crashes if wrong type delivered
|
||
- What: `BGTask.runFillInMissingDatesTask(task: task as! BGProcessingTask)` — force cast inside BGTaskScheduler callback. If scheduler delivers a different task type, crashes with no recovery.
|
||
- Impact: App crashes in the background task handler; system may reduce background execution privileges.
|
||
|
||
**`Shared/FeelsApp.swift`:110-117** | BUG | Fire-and-forget `Task.detached` blocks pile up on rapid scene transitions
|
||
- What: Multiple `Task.detached` closures for `scheduleBasedOnCurrentTime()` and `processPendingSideEffects()` have no cancellation handle. Rapid foreground transitions queue concurrent tasks, each mutating shared state.
|
||
- Impact: Multiple concurrent mutations to subscription status, Live Activity scheduling, and pending side effects.
|
||
|
||
**`Shared/Views/SettingsView/LiveActivityPreviewView.swift`:203-212** | WARNING | Timer callback mutates `@State` without actor guarantee
|
||
- What: `Timer.scheduledTimer` callback directly sets `@State` properties. While timers on the main run loop fire on the main thread, there is no enforcement that `startAnimation()` is only ever called on the main thread.
|
||
- Impact: If called from a non-main context, mutates `@State` off the main actor. SwiftUI purple thread-checker warning.
|
||
|
||
**`Shared/IAPManager.swift`:465-474** | WARNING | `Task.detached { [weak self] }` on a singleton — misleading ownership semantics
|
||
- What: `listenForTransactions()` returns `Task.detached { [weak self] in ... }`. On a singleton, `[weak self]` never becomes nil. The `deinit { updateListenerTask?.cancel() }` is also dead code. Future code added directly in the loop body without `await self?.` would be a data race.
|
||
- Impact: Misleading code contract; maintenance hazard.
|
||
|
||
**`Shared/AppDelegate.swift`:36-40** | WARNING | `@preconcurrency` suppresses concurrency violations in UNUserNotificationCenter delegate
|
||
- What: Extension annotated `@preconcurrency` to silence Swift concurrency warnings. Under Swift 6, `@preconcurrency` suppresses the violation rather than fixing it.
|
||
- Impact: Hidden concurrency violations in notification delegate callbacks.
|
||
|
||
**`Shared/Services/MoodStreakActivity.swift`:306-316** | WARNING | `scheduleStart` Timer `guard let self` is silent no-op on dealloc — no fallback
|
||
- What: If `self` is nil when timer fires, Live Activity start silently never happens with no fallback or logging.
|
||
- Impact: Live Activities may silently fail to start after scheduler recreation.
|
||
|
||
**`Shared/MoodStreakActivity.swift`:324-329** | WARNING | `scheduleEnd` Timer captures singletons without `[weak self]`
|
||
- What: End timer closure captures singletons by strong reference; timer never cancelled on `deinit` (singleton never deallocates).
|
||
- Impact: Leaked timer. If singleton semantics change, this becomes a retain cycle.
|
||
|
||
**`Shared/Services/MoodLogger.swift`:113** | WARNING | `getData(startDate:endDate:includedDays:)` called with empty `[]` for `includedDays` — behavior undefined
|
||
- What: Empty `includedDays` array semantics unknown — could mean "no days" rather than "all days". If it means "no days", `processPendingSideEffects()` never catches up on missed side effects.
|
||
- Impact: Widget or watch-logged moods may never have side effects (Live Activity, HealthKit, streak) applied.
|
||
|
||
**`Shared/Persisence/DataControllerGET.swift`:18, `Shared/Persisence/DataControllerDELETE.swift`:49, `Shared/Services/ExtensionDataProvider.swift`:113** | BUG | Off-by-one inclusive upper bound `<=` includes entries from next day's midnight
|
||
- What: Predicates use `entry.forDate <= endDate` where `endDate` is midnight of the next day. Entries logged exactly at midnight are included in the wrong day's query.
|
||
- Impact: Entries logged at midnight appear in wrong day's data across calendar views, month summaries, and streak calculations.
|
||
|
||
**`Shared/Views/SharingTemplates/*.swift` (7 files)** | WARNING | `var image: UIImage` calling `ImageRenderer`-based `asImage()` without `@MainActor` annotation
|
||
- What: Seven sharing template types expose `var image: UIImage` as a plain computed property calling `someView.asImage(size:)`. If `asImage()` uses `ImageRenderer` (which is `@MainActor`-isolated), this is a Swift 6 actor isolation violation.
|
||
- Impact: Compile-time error under strict concurrency. At runtime currently safe since called from button actions on main thread.
|
||
|
||
---
|
||
## SOURCE: SwiftUI Performance Auditor (33 findings)
|
||
|
||
**`Shared/Onboarding/views/OnboardingTime.swift`:13-17** | PERFORMANCE | `DateFormatter` computed property allocated every body re-render at 60fps during date picker interaction
|
||
- What: `var formatter: DateFormatter { let dateFormatter = DateFormatter(); ... return dateFormatter }` is a computed property accessed directly in `body`. The `DatePicker` bound to `$onboardingData.date` updates on every wheel scroll tick, triggering a body re-render every frame.
|
||
- Impact: During date-picker interaction, formatter reallocated ~60 times/second wasting CPU on repeated ICU initializations.
|
||
|
||
**`Shared/Onboarding/views/OnboardingWrapup.swift`:15-19** | PERFORMANCE | `DateFormatter` computed property allocated every body re-render
|
||
- What: Same pattern as `OnboardingTime`. `var formatter: DateFormatter` is a computed property creating a new instance each access.
|
||
- Impact: Repeated expensive allocations on every render.
|
||
|
||
**`FeelsWidget2/FeelsTimelineWidget.swift`:73-83** | PERFORMANCE | `DateFormatter` created as computed property on widget views — reallocated on every render pass
|
||
- What: `private var dayFormatter: DateFormatter` and `private var dateFormatter: DateFormatter` are computed properties constructing new `DateFormatter` instances. Repeated in `SmallWidgetView`, `MediumWidgetView`, and `LargeWidgetView`.
|
||
- Impact: Widget rendering is memory-constrained. Multiple `DateFormatter` objects allocated and immediately discarded per timeline refresh.
|
||
|
||
**`FeelsWidget2/FeelsTimelineWidget.swift`:165-170** | PERFORMANCE | `headerDateRange` creates another `DateFormatter` inside a computed property called in body
|
||
- What: `private var headerDateRange: String` creates a fresh `DateFormatter` inline per call. Third `DateFormatter` allocation per `MediumWidgetView` render.
|
||
- Impact: Compounding allocations in memory-limited widget extension.
|
||
|
||
**`FeelsWidget2/FeelsVoteWidget.swift`:64-76** | PERFORMANCE | `votingDateString` computed property creates two `DateFormatter` instances per call
|
||
- What: Constructs `dayFormatter` and `dateFormatter` inside the function body. Accessed directly from `body`.
|
||
- Impact: Two `DateFormatter` allocations per body evaluation including locale-sensitive ICU initialization.
|
||
|
||
**`Shared/Views/DayView/DayView.swift`:99-103** | PERFORMANCE | `sortedGroupedData` recomputes expensively on every body pass with no caching
|
||
- What: `private var sortedGroupedData` calls `.sorted { $0.key > $1.key }` twice and constructs a new array on every body evaluation. Unlike `MonthView` which caches in `@State`, `DayView` re-sorts on every render.
|
||
- Impact: O(n log n) work on every render for all users with multiple years of data.
|
||
|
||
**`Shared/Views/MonthView/MonthView.swift`:173-175** | PERFORMANCE | `displayData.flatMap` runs in body every render — recreates full flattened month array unconditionally
|
||
- What: `let allMonths = displayData.flatMap { ... }` runs on every body evaluation, creating a new array even on trivial state changes.
|
||
- Impact: O(n) allocation for users with several years of data on every render including trivial changes.
|
||
|
||
**`Shared/Views/MonthView/MonthView.swift`:176** | BUG | `ForEach` uses `id: \.element.month` — non-unique across years, causes diffing collisions
|
||
- What: `ForEach(Array(allMonths.enumerated()), id: \.element.month)` uses integer month number (1–12) as identity. January 2023 and January 2024 both have `.month == 1`.
|
||
- Impact: SwiftUI may skip rendering new month cards, animate between wrong cells, or fail to insert/remove months on data changes for users with more than one year of data.
|
||
|
||
**`Shared/Views/MonthView/MonthView.swift`:410** | PERFORMANCE | `@ObservedObject private var demoManager = DemoAnimationManager.shared` inside `MonthCard` — 60fps singleton updates fan to all cards
|
||
- What: Every `MonthCard` holds an `@ObservedObject` reference to the same singleton. When `demoManager.animationProgress` publishes at 60 fps during demo mode, all `MonthCard` views receive `objectWillChange`, including off-screen cards.
|
||
- Impact: Dozens of simultaneous subscription notifications per frame during demo animation.
|
||
|
||
**`Shared/Views/MonthView/MonthView.swift`:431-443** | PERFORMANCE | `animatedMetrics` computed property calls `demoManager.visiblePercentageForMonth` on every body pass during animation
|
||
- What: Reconstructs full `[MoodMetrics]` array via `.map` on every evaluation. During demo animation at 60 fps, runs for every card in the list on every frame.
|
||
- Impact: O(n_months × 5_moods × 60fps) object allocations per second during demo playback.
|
||
|
||
**`Shared/Views/MonthView/MonthView.swift`:661** | BUG | `cachedMetrics` only recalculated when empty — stale metrics after entry edits
|
||
- What: `if cachedMetrics.isEmpty { cachedMetrics = ... }` in `.onAppear`. If user edits a mood entry, `entries` changes but `cachedMetrics` is non-empty so never recalculates. `Equatable` conformance checks only count, not content.
|
||
- Impact: After editing a mood entry, `MonthCard` shows old mood distribution stats. Share sheet also uses `cachedMetrics`, producing incorrect shared images.
|
||
|
||
**`Shared/Views/YearView/YearView.swift`:334-342** | BUG | `YearCard.Equatable` compares `yearEntries.count` only — stale cached metrics after data edits
|
||
- What: `YearCard.static func ==` checks only count. `cachedMetrics` calculated only when empty. If entry moods change without changing count, `YearCard` never re-renders.
|
||
- Impact: Year statistics and share images show incorrect mood distribution after any edit.
|
||
|
||
**`Shared/Views/YearView/YearView.swift`:174-178** | PERFORMANCE | `GeometryReader` as preference-key scroll offset tracker — fires on every scroll pixel
|
||
- What: `GeometryReader` embedded in scroll content `.background` to measure scroll offset and emit via preference key. Fires `onPreferenceChange` on every single scroll position change. Same pattern in `MonthView`.
|
||
- Impact: Every scroll position change triggers a preference update at 60+ fps → main thread callback → potential animation state mutation.
|
||
|
||
**`Shared/Views/YearView/YearViewModel.swift`:36-38** | PERFORMANCE | O(n log n) sort to find minimum date when `min(by:)` is O(n)
|
||
- What: `filteredEntries.sorted(by: { $0.forDate < $1.forDate }).first?.forDate` sorts entire array to get earliest date.
|
||
- Impact: Unnecessary sort on every `updateData()` call. `min(by:)` achieves same result in O(n).
|
||
|
||
**`Shared/Views/YearView/YearViewModel.swift`:48-51** | PERFORMANCE | `data.removeAll()` and `entriesByYear.removeAll()` trigger two separate SwiftUI layout passes with empty state
|
||
- What: In `filterEntries`, two separate `removeAll()` calls each publish `objectWillChange` before new data is assigned.
|
||
- Impact: View briefly renders empty-data state (showing `EmptyHomeView`) twice on every year data refresh.
|
||
|
||
**`Shared/Views/CelebrationAnimations.swift`:311** | PERFORMANCE | `Double.random(in:)` called inside body inside `ForEach` — non-deterministic opacity on every render
|
||
- What: `ShatterReformAnimation.body` applies `.fill(mood.color.opacity(Double.random(in: 0.5...1.0)))` inside `ForEach`. Generates new random opacity on every body evaluation.
|
||
- Impact: Shard colors re-randomize on every state change causing visible flickering between animation phases.
|
||
|
||
**`Shared/Views/EntryListView.swift`:139,146** | PERFORMANCE | `classicStyle` bypasses `DateFormattingCache` — repeated ICU operations on every body evaluation
|
||
- What: `classicStyle` calls `Random.weekdayName` and `Random.dayFormat` directly, bypassing `DateFormattingCache.shared`. Same bypass in `microStyle` and `MotionCardView`.
|
||
- Impact: `classicStyle` is default and most common. With 365+ cells in the list, cache misses cause redundant date formatting on every redraw.
|
||
|
||
**`Shared/Views/EntryListView.swift`:2249** | PERFORMANCE | CMMotion callback calls `withAnimation(.interactiveSpring(...))` on every accelerometer update at 30 Hz
|
||
- What: The `motionManager.startDeviceMotionUpdates(to: .main)` callback wraps every accelerometer reading in `withAnimation(.interactiveSpring(...))`, re-enqueuing an animation transaction at 30 Hz.
|
||
- Impact: 30 animation transactions per second on the main thread for all visible `MotionCardView` cells simultaneously.
|
||
|
||
**`Shared/Views/FeelsSubscriptionStoreView.swift`:165-177** | PERFORMANCE | Multiple `withAnimation(.repeatForever(...))` started in `.onAppear` — cannot cancel on dismiss
|
||
- What: Every theme marketing view (12 total) starts one or more `repeatForever` animations in `.onAppear`. `.onDisappear` resets state values but cannot cancel in-flight repeating animation transactions.
|
||
- Impact: Rapid open/close cycles stack animation transactions, causing visual glitches and CPU overhead.
|
||
|
||
**`Shared/Views/LockScreenView.swift`:1121-1128** | PERFORMANCE | `CGFloat.random(in:)` called inside body for cloud sizes — re-randomizes on every state change
|
||
- What: `ForecastLockBackground` uses `CGFloat.random(in: 40...80)` inline inside the view body for cloud element sizes and blur values. Not captured in `@State` or `onAppear`.
|
||
- Impact: Cloud sizes change on every body re-render, producing visual instability during authentication flow.
|
||
|
||
**`FeelsWidget2/FeelsTimelineWidget.swift`:87-92** | PERFORMANCE | `TimeLineCreator.createViews` and `UserDefaultsStore.moodTintable()` called synchronously in widget `init`
|
||
- What: Widget view `init` calls `TimeLineCreator.createViews(daysBack:)` and `UserDefaultsStore.moodTintable()` synchronously across all three widget size classes.
|
||
- Impact: Blocking synchronous I/O on WidgetKit's render thread. Contention with main app's ModelContainer can cause widget render deadline timeout.
|
||
|
||
**`Shared/Views/DayView/DayView.swift`:89** | PERFORMANCE | `UserDefaultsStore.getOnboarding()` called inside `headerView` computed property on every body pass
|
||
- What: `headerView` calls `ShowBasedOnVoteLogics.isMissingCurrentVote(onboardingData: UserDefaultsStore.getOnboarding())` on every body pass. Decode path reachable on cache miss.
|
||
- Impact: Every body re-render of `DayView` calls into `UserDefaultsStore.getOnboarding()`. Not thread-safe.
|
||
|
||
**`Shared/Views/CustomizeView/CustomizeView.swift`:295** | WARNING | Hidden `Text` hack to force re-renders — fragile, may be optimized away by future OS
|
||
- What: `Text(String(customMoodTintUpdateNumber)).hidden().frame(height: 0)` inserted as workaround to trigger re-renders. SwiftUI anti-pattern.
|
||
- Impact: Future OS optimizations of hidden views may silently break this, causing tint changes to stop reflecting in picker UI.
|
||
|
||
**`Shared/Views/MonthView/MonthView.swift`:246-259** | PERFORMANCE | `AnyView` type erasure for conditional `.mask` modifier — forces full re-creation on paywall state change
|
||
- What: `.mask` modifier uses `(condition) ? AnyView(LinearGradient(...)) : AnyView(Color.black)`. SwiftUI cannot track structural identity through `AnyView`. Same pattern in `YearView` at lines 184-195.
|
||
- Impact: Entire `ScrollView` content re-created from scratch on paywall state changes instead of targeted update.
|
||
|
||
**`Shared/Views/SharingListView.swift`:51** | PERFORMANCE | Heavy use of `AnyView` for all sharable items — disables SwiftUI structural diffing for entire sharing list
|
||
- What: `WrappedSharable` structs use `AnyView` for `.preview` and `.destination`. `ForEach` over `AnyView` items means no structural identity comparison.
|
||
- Impact: Any state change causes all sharing item previews to be fully re-created.
|
||
|
||
**`Shared/Models/MoodTintable.swift`:112-143** | PERFORMANCE | `UserDefaultsStore.getCustomMoodTint()` called up to 10 times per render for custom tint views
|
||
- What: `CustomMoodTint.color(forMood:)` and `CustomMoodTint.secondary(forMood:)` each call `UserDefaultsStore.getCustomMoodTint()` per invocation. A full month card calls it 10 times.
|
||
- Impact: For custom tint users, every cell render invokes multiple JSON decode cycles. 365 cells = thousands of UserDefaults reads before first frame.
|
||
|
||
**`Shared/Views/DemoAnimationManager.swift`:91-95** | PERFORMANCE | `Timer.scheduledTimer` at 1/60s spawns new `Task { @MainActor in }` per tick — tasks queue faster than consumed
|
||
- What: Timer fires every `1/60` second; each fire wraps `updateProgress()` in `Task { @MainActor in ... }`. Under load, Tasks queue up and multiple `updateProgress()` calls fire for the same frame.
|
||
- Impact: Animation progress applied multiple times per visual frame causing stutters, jumps, or skips.
|
||
|
||
**`FeelsWidget2/FeelsVoteWidget.swift`:55-176** | PERFORMANCE | `moodTint` and `moodImages` computed props each call `UserDefaultsStore` on every body access
|
||
- What: `private var moodTint` and `private var moodImages` are computed properties triggering UserDefaults reads on every access. Called multiple times within `body`.
|
||
- Impact: 4–6 UserDefaults reads plus potential JSON decoding per widget render cycle. Increases likelihood of exceeding WidgetKit render budget.
|
||
|
||
**`FeelsWidget2/FeelsIconWidget.swift`:50-58** | PERFORMANCE | `customWidget` computed property called twice in body — reads UserDefaults twice per render
|
||
- What: `customWidget` reads UserDefaults each access; called twice in `body`, doubling I/O per render.
|
||
- Impact: Doubles UserDefaults I/O per render in the memory-constrained widget extension.
|
||
|
||
**`Shared/MoodLogger.swift`:134,141** | PERFORMANCE | `ISO8601DateFormatter()` instantiated fresh on every call
|
||
- What: `MoodLogger` creates a new `ISO8601DateFormatter()` at lines 134 and 141 inside date key functions. Called every time the user logs a mood.
|
||
- Impact: `ISO8601DateFormatter` has similar initialization cost to `DateFormatter`. Should be `static let` properties.
|
||
|
||
**`Shared/Views/ExportView.swift`:177-178** | PERFORMANCE | `DateFormatter` created inside `dateRangeText` computed property on every call
|
||
- What: `private var dateRangeText: String` creates `DateFormatter` inline. Called from `body` during export progress updates.
|
||
- Impact: Each `ExportView` body evaluation allocates and discards a `DateFormatter`.
|
||
|
||
**`Shared/Views/AddMoodHeaderView.swift`:343-344** | WARNING | `repeatForever` animation started in `.onAppear` — `.onDisappear` resets value but cannot cancel in-flight repeat
|
||
- What: `OrbitVotingView.onAppear` starts `repeatForever` animation on `centerPulse`. `.onDisappear` sets `centerPulse = 1.0` nominally stopping it. Rapid navigation can cause animation to restart from intermediate state. Same pattern in `NeonVotingView` lines 553-558.
|
||
- Impact: Rapid navigation to/from day view causes orbit pulse to visually jump or stutter.
|
||
|
||
**`FeelsWidget2/FeelsTimelineWidget.swift`:236** | PERFORMANCE | `Date(timeIntervalSince1970: 0)` in `LargeWidgetView` fetches ALL entries since Unix epoch
|
||
- What: Large widget fetches entries with `startDate: Date(timeIntervalSince1970: 0)` — no lower-bound date filter, loading every mood entry the user has ever logged into the widget extension's memory.
|
||
- Impact: Long-term users with years of data can have thousands of `MoodEntryModel` objects loaded into the widget extension — a severely memory-constrained process. Can be killed by jetsam, resulting in blank widget.
|
||
|
||
**`Shared/Views/MonthView/MonthView.swift`:342-344,362-365** | WARNING | Two separate `.onAppear` modifiers on same view root — ordering not guaranteed
|
||
- What: Two sequential `.onAppear` modifiers: one fires analytics, one populates `cachedSortedData`. SwiftUI ordering of multiple `.onAppear` handlers is implementation-defined.
|
||
- Impact: If Apple changes execution order, analytics could fire before data is available.
|
||
|
||
---
|
||
## SOURCE: Security Auditor (33 findings)
|
||
|
||
**`Shared/Analytics.swift`:24** | CRITICAL | Hardcoded PostHog API key committed to source control
|
||
- What: `private static let apiKey = "phc_3GsB3oqNft8Ykg2bJfE9MaJktzLAwr2EPMXQgwEFzAs"` is a live production analytics key stored as a source-level string literal, permanently captured in git history.
|
||
- Impact: Any party with the key can inject events into PostHog, poison analytics dashboards, and potentially enumerate device/user data via the PostHog API.
|
||
|
||
**`Shared/GoogleService-Info.plist`:10** | CRITICAL | Firebase/Google API key and project credentials in source control
|
||
- What: `AIzaSyDRIs9buGGPWtECfyhCgFsNvYzvmQnhDr0` (Firebase API key), `CLIENT_ID`, `GCM_SENDER_ID`, and `GOOGLE_APP_ID` committed in a checked-in plist. Firebase appears to be removed from the codebase but the credential file was not deleted.
|
||
- Impact: Active Google API key permanently in git history. Allows unauthenticated Firebase Auth, Storage, and GCM calls. Should be revoked in Firebase console and deleted from repository.
|
||
|
||
**`Shared/Analytics.swift`:67-76** | CRITICAL | Session replay captures UI screenshots in production with images unmasked and enabled by default
|
||
- What: `config.sessionReplayConfig.screenshotMode = true`, `config.sessionReplayConfig.maskAllImages = false`, and `config.captureNetworkTelemetry = true` set in PostHog configuration. Session replay enabled by default for new users (opt-out required, not opt-in).
|
||
- Impact: PostHog screenshot-mode session replay can capture full screen contents — including mood entries, journal notes, and photos — without user consent presented upfront. Potential GDPR/CCPA violation for sensitive mental health data.
|
||
|
||
**`Shared/Services/BiometricAuthManager.swift`:17** | CRITICAL | `isUnlocked` defaults to `true` — locked content briefly visible before auth runs
|
||
- What: `@Published var isUnlocked: Bool = true`. Views gated on `isUnlocked` are rendered and visible before the biometric prompt appears.
|
||
- Impact: On app foreground, mood data is momentarily visible before Face ID/Touch ID challenge completes, even with lock enabled.
|
||
|
||
**`Shared/Services/BiometricAuthManager.swift`:172-175** | CRITICAL | `disableLock()` requires no re-authentication
|
||
- What: `disableLock()` directly sets `isLockEnabled = false` and `isUnlocked = true` with no biometric or passcode challenge.
|
||
- Impact: An attacker with brief physical access to an unlocked app can permanently disable privacy lock with a single tap in Settings.
|
||
|
||
**`Shared/Persisence/ExtensionDataProvider.swift`:199-217** | CRITICAL | Non-atomic delete-then-insert can permanently delete user mood data
|
||
- What: Existing entries are deleted and `try? context.save()` is called (line 205). If this succeeds but the subsequent insert save fails (line 217), the original entries are permanently deleted with no recovery path.
|
||
- Impact: User loses all mood entries for a given date — the delete was persisted but the insert was not. No error surface, no retry, no rollback.
|
||
|
||
**`Shared/Persisence/DataControllerADD.swift`:13-20** | CRITICAL | Non-atomic delete-save-insert in main app path — same data loss risk
|
||
- What: `add()` deletes all entries for a date, calls `try? modelContext.save()`, then inserts and calls `saveAndRunDataListeners()`. If the final save fails, deleted entries cannot be recovered.
|
||
- Impact: User mood data silently lost on save failure during mood logging.
|
||
|
||
**`Shared/Random.swift`:30,32** | CRITICAL | Force-unwrap on `UserDefaults(suiteName:)` for all app group defaults
|
||
- What: `GroupUserDefaults.groupDefaults` force-unwraps `UserDefaults(suiteName:)!`. Called on almost every UI render cycle and data operation.
|
||
- Impact: App crashes at startup and on every subsequent access if app group entitlement is misconfigured, revoked, or unavailable.
|
||
|
||
**`Shared/Views/SettingsView/SettingsTabView.swift`:61-66** | CRITICAL | `CustomizeContentView` missing required `@EnvironmentObject` injections — guaranteed crash
|
||
- What: `CustomizeContentView()` used inside `SettingsTabView` without `.environmentObject(authManager)` or `.environmentObject(iapManager)`. Child views declare `@EnvironmentObject var iapManager: IAPManager`.
|
||
- Impact: App crashes at runtime with fatal "No ObservableObject of type IAPManager found" when user navigates to Customize tab from Settings.
|
||
|
||
**`Shared/Analytics.swift`:160-163** | WARNING | Privacy-sensitive settings transmitted as super-properties to PostHog on every event
|
||
- What: `privacy_lock_enabled` and `healthkit_enabled` are registered as super-properties attached to every analytics event, along with theme, icon_pack, voting_layout, day_view_style, mood_shape, and personality_pack values.
|
||
- Impact: Device configuration and security-feature state attached to every PostHog capture event. May conflict with App Store privacy disclosures if not declared.
|
||
|
||
**`Shared/Analytics.swift`:255-262** | BUG | Race condition in analytics opt-out: SDK can be left opted-out while flag shows opted-in
|
||
- What: `optIn()` first writes `false` to UserDefaults, then calls `PostHogSDK.shared.optIn()`. If the app crashes between these two lines, inconsistent state persists until next explicit toggle.
|
||
- Impact: User believes analytics are disabled but events are being dropped, or vice versa.
|
||
|
||
**`Shared/Services/BiometricAuthManager.swift`:108-110** | WARNING | Cancelled biometric prompt silently escalates to passcode without user consent
|
||
- What: When Face ID throws (user taps Cancel), the catch block unconditionally calls `authenticateWithPasscode()` if `canUseDevicePasscode` is true.
|
||
- Impact: A user who cancels Face ID expecting to deny access is immediately presented with a passcode prompt — non-standard iOS authentication UX.
|
||
|
||
**`Shared/IAPManager.swift`:206-212** | WARNING | Cached subscription expiration unconditionally trusted offline — no cryptographic verification
|
||
- What: `cachedSubscriptionExpiration` is a plain `Date` in GroupUserDefaults. If the stored date is in the future, `state = .subscribed(...)` is set without re-verifying with StoreKit.
|
||
- Impact: GroupUserDefaults accessible to any app in the same app group. Modification of the expiration date grants permanent subscription access.
|
||
|
||
**`Shared/IAPManager.swift`:233-244** | WARNING | `restoreCachedSubscriptionState()` grants `.subscribed` with no expiration from UserDefaults boolean alone
|
||
- What: If `hasActive` is `true` and `cachedExpiration` is `nil`, grants `state = .subscribed(expirationDate: nil, willAutoRenew: false)` unconditionally.
|
||
- Impact: `nil` expiration means the widget always shows premium UI regardless of actual entitlement.
|
||
|
||
**`Shared/Models/UserDefaultsStore.swift`:217,221-234** | WARNING | `try?` silently discards all Codable decode errors for persisted user settings
|
||
- What: JSON decode failures for onboarding data, custom widgets, and custom mood tints all swallowed with `try?`. Functions return default values with no indication stored data was discarded.
|
||
- Impact: Data corruption in UserDefaults silently loses user customization state with no error message.
|
||
|
||
**`Shared/Models/UserDefaultsStore.swift`:217** | WARNING | Static cache `cachedOnboardingData` has no thread synchronization
|
||
- What: `private static var cachedOnboardingData: OnboardingData?` is a static mutable variable accessed from both main thread and background tasks.
|
||
- Impact: Concurrent reads/writes constitute a data race. Can corrupt cache or read partially-initialized state.
|
||
|
||
**`Shared/Random.swift`:48,68,174** | WARNING | Mutable static dictionaries have no thread-safety protection
|
||
- What: `static var existingWeekdayName`, `static var existingDayFormat`, and `static var textToImageCache` are mutable static dictionaries accessible to any context including widget extension background rendering.
|
||
- Impact: Concurrent access can corrupt dictionaries, producing wrong weekday names, wrong day formats, or cached images mapped to incorrect keys.
|
||
|
||
**`Shared/Random.swift`:65** | BUG | `monthName(fromMonthInt:)` crashes when `fromMonthInt` is 0 or out of range
|
||
- What: `monthSymbols[fromMonthInt-1]` with no bounds check. Passing `0` produces `[-1]` — fatal index out of bounds crash.
|
||
- Impact: Any code path that constructs a month integer from raw/corrupted data will crash the app.
|
||
|
||
**`Shared/Services/WatchConnectivityManager.swift`:117-124** | WARNING | `pendingMoods` array accessed from multiple threads without synchronization
|
||
- What: Written from Watch main thread and read/mutated from WCSession delegate background queue with no synchronization.
|
||
- Impact: Concurrent access can corrupt the `pendingMoods` array, causing mood entries to be dropped, duplicated, or invalid indices accessed.
|
||
|
||
**`Shared/Analytics.swift`:67** | WARNING | `captureElementInteractions = true` auto-captures all UI interaction events
|
||
- What: PostHog auto-captures taps, element names, and screen transitions without explicit event definitions.
|
||
- Impact: Button labels, navigation paths, and element identifiers sent to PostHog automatically. If any label contains user-generated content, it could be inadvertently captured.
|
||
|
||
**`Shared/Analytics.swift`:552** | BUG | Force-unwrap on Optional after nil check in event payload construction
|
||
- What: `error != nil ? ["error": error!] : nil` — force-unwrap after explicit nil check is an unsafe pattern. Future refactors could move the force-unwrap outside the conditional.
|
||
- Impact: Crash if pattern is modified without understanding the guard structure.
|
||
|
||
**`Shared/FeelsApp.swift`:30** | BUG | Force cast `task as! BGProcessingTask` inside BGTaskScheduler handler
|
||
- What: If system delivers a different task type, crashes at runtime with no recovery.
|
||
- Impact: App crash during background processing.
|
||
|
||
**`Shared/Services/HealthService.swift`:79-80** | BUG | `isAuthorized` set to `true` regardless of whether HealthKit permission was granted
|
||
- What: After `try await healthStore.requestAuthorization(toShare:read:)` returns without throwing, `isAuthorized = true` and `isEnabled = true` are set. HealthKit does not throw if user denies permissions.
|
||
- Impact: App displays "HealthKit connected" and attempts HealthKit saves even when user denied permissions. HealthKit writes silently fail while UI shows sync as enabled.
|
||
|
||
**`Shared/Services/BiometricAuthManager.swift`:105,129,149,167** | WARNING | Biometric auth errors logged via `print()` not structured logger
|
||
- What: All error paths in auth functions use `print(...)` instead of `AppLogger.biometrics`. Security-relevant error descriptions visible in console in production builds.
|
||
- Impact: LAError codes visible in crash logs shared with third parties.
|
||
|
||
**`Shared/Services/BiometricAuthManager.swift` (class level)** | WARNING | `isLockEnabled` stored in GroupUserDefaults accessible to widget and watch extensions
|
||
- What: Lock state is in the shared app group suite, readable and writable by widget and watch extensions.
|
||
- Impact: A compromised widget/watch extension can read whether privacy lock is enabled and write `false` to disable it, bypassing biometric lock without triggering main app audit trail.
|
||
|
||
**`Shared/IAPManager.swift`:70** | WARNING | `firstLaunchDate` returns `Date()` (now) when key is absent — trial calculation uses wrong time
|
||
- What: `GroupUserDefaults.groupDefaults.object(forKey: .firstLaunchDate) as? Date ?? Date()`. If key missing (cleared or fresh install), trial window calculated from right now.
|
||
- Impact: Reinstalling or clearing UserDefaults can permanently reset the trial period, granting a new 30-day trial on demand.
|
||
|
||
**`Shared/Persisence/DataControllerDELETE.swift`:22-40** | WARNING | `deleteLast` and `deleteRandomFromLast` are production-accessible with no authorization gate
|
||
- What: `deleteRandomFromLast(numberOfEntries:)` randomly deletes user mood entries. Both functions are protocol-level requirements available in production builds with no `#if DEBUG` guard.
|
||
- Impact: Any code path that accidentally calls `deleteRandomFromLast` irreversibly destroys user mood history with no recovery.
|
||
|
||
**`Shared/Views/SettingsView/SettingsView.swift`:1158** | BUG | Force-unwrap URL from user-controlled path component
|
||
- What: `URL(string: "shareddocuments://\(url.path)")!` where `url.path` is a runtime-generated file URL path that can contain spaces or special characters.
|
||
- Impact: App crashes when export path contains any character that invalidates the URL.
|
||
|
||
**`Shared/Views/CustomizeView/CustomizeView.swift`:548-556** | WARNING | NSFW personality pack age gate is commented out
|
||
- What: The conditional block gating access to the "Rude" personality pack behind an over-18 confirmation has been commented out. All personality packs accessible without any age check.
|
||
- Impact: Users of all ages can select adult-language notification personality packs. App Store compliance issue if marketed to minors.
|
||
|
||
**`Shared/Onboarding/views/OnboardingSubscription.swift`:139-145** | BUG | Onboarding completion fires unconditionally on subscription sheet dismiss
|
||
- What: `onDismiss` handler calls `completionClosure(onboardingData)` regardless of whether user subscribed. `onboardingCompleted` analytics fires on both skip and dismiss paths, producing double-fire.
|
||
- Impact: Onboarding marked complete even if user closed paywall without subscribing. Corrupts onboarding funnel metrics.
|
||
|
||
**No `.xcprivacy` file found in project** | CRITICAL | Missing PrivacyInfo.xcprivacy manifest — App Store compliance violation
|
||
- What: There is no `.xcprivacy` file anywhere in the project tree. The app uses PostHog session replay (screenshots), CoreMotion accelerometer, HealthKit, photos, notifications, and UserDefaults — all requiring privacy manifest declarations per App Store requirements effective May 2024.
|
||
- Impact: Apple will reject app updates submitted without a privacy manifest declaring all required reason APIs and third-party SDKs (PostHog). App is out of compliance with current App Store review policy.
|
||
|
||
**`Shared/Analytics.swift`:36-50** | WARNING | Session replay enabled by default — opt-out required, not opt-in
|
||
- What: `sessionReplayEnabled` returns `true` when key absent (first launch). New user's session screen-recorded from first app open until explicitly disabled in Settings.
|
||
- Impact: May violate GDPR Article 7 and CCPA, especially given sensitive nature (mental health data) of what is displayed on screen.
|
||
|
||
**`Shared/Services/PhotoManager.swift`:104-134** | WARNING | Synchronous disk I/O for photo loading on main actor
|
||
- What: `loadPhoto(id:)` and `loadThumbnail(id:)` call `Data(contentsOf: fullURL)` synchronously on the main actor. Photos can be megabytes in size.
|
||
- Impact: UI freezes for duration of each photo disk read. Visible jank when day view scrolls past photo entries.
|
||
|
||
|
||
---
|
||
|
||
## Accessibility Auditor (46 findings)
|
||
|
||
| Severity | File | Finding |
|
||
|----------|------|---------|
|
||
| CRITICAL | `Shared/Views/LockScreenView.swift`:1615 | No accessibility labels on any element — VoiceOver users cannot unlock the app |
|
||
| CRITICAL | `Shared/Views/AddMoodHeaderView.swift`:515 | `NeonVotingView` has no `accessibilityElement` container — Canvas decoratives fully traversable |
|
||
| BUG | `Shared/Views/YearView/YearView.swift`:724 | `YearHeatmapCell` label is only "Mood entry" — no date, mood, or button trait |
|
||
| BUG | `Shared/Views/YearView/YearView.swift`:718 | `YearHeatmapCell` tappable but has no `.isButton` trait or hint |
|
||
| BUG | `Shared/Views/CustomizeView/SubViews/PersonalityPackPickerView.swift`:52 | `.alert` inside `ForEach` — only last row's alert fires |
|
||
| BUG | `Shared/Views/SettingsView/SettingsTabView.swift`:61 | `CustomizeContentView` missing `iapManager`/`authManager` environment objects — guaranteed crash |
|
||
| BUG | `Shared/Views/SettingsView/LiveActivityPreviewView.swift`:325 | `ImageRenderer` used on background thread — produces corrupt images |
|
||
| BUG | `Shared/Views/CelebrationAnimations.swift`:109 | `onComplete()` fires after view dismissal — potential duplicate mood entry |
|
||
| BUG | `Shared/Views/LockScreenView.swift`:1694 | Second biometric failure shows no error — user stuck on lock screen |
|
||
| BUG | `Shared/Views/Views/IAPWarningView.swift`:56 | `FeelsSubscriptionStoreView` sheet missing `iapManager` `@EnvironmentObject` |
|
||
| BUG | `Shared/Views/SharingStylePickerView.swift`:206 | `designs[selectedIndex]` without bounds check — crashes when designs is empty |
|
||
| BUG | `Shared/Views/PhotoPickerView.swift`:151 | `isProcessing = true` mutated from non-`@MainActor` async context |
|
||
| BUG | `Shared/Views/SharingTemplates/WeekTotalTemplate.swift`:30 | Raw developer string "WeekTotalTemplate body" shown to VoiceOver users |
|
||
| WARNING | `Shared/Views/MonthView/MonthView.swift`:39 | Weekday headers are single ambiguous chars ("T", "S") — duplicate IDs for VoiceOver |
|
||
| WARNING | `Shared/Views/MonthView/MonthView.swift`:679 | `.accessibilityHint("Double tap to edit")` is hardcoded English, not localized |
|
||
| WARNING | `Shared/Views/EntryListView.swift`:97 | Accessibility hints use inline `String(localized:)` — may not translate |
|
||
| WARNING | `Shared/Views/EntryListView.swift`:560 | Chronicle style hardcoded English strings not localized |
|
||
| WARNING | `Shared/Views/EntryListView.swift`:722 | Neon style renders `"NO_DATA"` raw developer string |
|
||
| WARNING | `Shared/Views/EntryListView.swift`:2242 | Accelerometer starts on `.onAppear`, never stopped |
|
||
| WARNING | `Shared/Views/DayView/DayView.swift`:401 | "SIDE A" hardcoded English decoration announced by VoiceOver |
|
||
| WARNING | `Shared/Views/DayView/DayView.swift`:556 | "avg" abbreviation not localized, no accessibility label override |
|
||
| WARNING | `Shared/Views/FeelsSubscriptionStoreView.swift`:129 | All paywall marketing copy hardcoded English — not localized |
|
||
| WARNING | `Shared/Views/FeelsSubscriptionStoreView.swift`:51 | `dismiss()` in async Task after status check — may dismiss wrong view |
|
||
| WARNING | `Shared/Onboarding/views/OnboardingTime.swift`:65 | `DatePicker` hardcoded `.colorScheme(.light)` — dark mode users see jarring white picker |
|
||
| WARNING | `Shared/Onboarding/views/OnboardingTime.swift`:87 | Reminder time sentence is hardcoded English, not localizable |
|
||
| WARNING | `Shared/Onboarding/views/OnboardingCustomizeOne.swift`:39 | `.foregroundColor(.black)` hardcoded — fails contrast in dark mode |
|
||
| WARNING | `Shared/Onboarding/views/OnboardingCustomizeTwo.swift`:37 | `.foregroundColor(.white)` hardcoded — fails contrast in light mode |
|
||
| WARNING | `Shared/Onboarding/views/OnboardingStyle.swift`:146 | `UIColor.darkText`/`darkGray` don't adapt to dark mode — WCAG contrast failure |
|
||
| WARNING | `Shared/Models/Theme.swift`:191 | `AlwaysLight.bgColor` returns dark background in dark system mode |
|
||
| WARNING | `Shared/Views/NoteEditorView.swift`:59 | Navigation title "Journal Note" hardcoded English |
|
||
| WARNING | `Shared/Views/NoteEditorView.swift`:91 | Mood icon in entry header has no accessibility label |
|
||
| WARNING | `Shared/Views/InsightsView/InsightsView.swift`:97 | Pull-to-refresh calls `refreshInsights()` without `await` — spinner dismisses before completion |
|
||
| WARNING | `Shared/Views/CustomizeView/SubViews/VotingLayoutPickerView.swift`:25 | "Voting Layout" section title hardcoded English |
|
||
| WARNING | `Shared/Views/CustomizeView/CustomizeView.swift`:168 | Customize rows use `.onTapGesture` without `.isButton` trait or hint |
|
||
| WARNING | `Shared/Views/CustomizeView/SubViews/ImagePackPickerView.swift`:5 | Icon pack rows have label only — no hint, no `.isButton` trait |
|
||
| WARNING | `Shared/Views/CustomizeView/SubViews/ShapePickerView.swift`:43 | Shape picker preview uses `randomElement()!` — accessibility label changes unpredictably |
|
||
| WARNING | `Shared/Views/CustomizeView/SubViews/DayFilterPickerView.swift`:12 | Singleton `DaysFilterClass.shared` in `@StateObject` — SwiftUI lifecycle bypassed |
|
||
| WARNING | `Shared/Views/SettingsView/SettingsView.swift`:1733 | Debug button misspelling "luanch" announced verbatim by VoiceOver |
|
||
| WARNING | `Shared/Views/SettingsView/SettingsView.swift`:1817 | Force-unwrap URL literals for Privacy Policy and EULA buttons |
|
||
| WARNING | `Shared/Views/Views/ExportView.swift`:100 | "Export Data" and "Cancel" hardcoded English |
|
||
| WARNING | `Shared/Views/Views/ExportView.swift`:319 | Error message "Failed to create export file" hardcoded English |
|
||
| WARNING | `Shared/Views/MonthView/MonthDetailView.swift`:40 | Share image rendered synchronously on main thread — blocks UI during VoiceOver share |
|
||
| WARNING | `Shared/Views/PurchaseButtonView.swift`:127 | "Payment Issue" badge text hardcoded English |
|
||
| WARNING | `Shared/Views/CustomizeView/SubViews/IconPickerView.swift`:73 | `setAlternateIconName` error silently swallowed — no user feedback |
|
||
| WARNING | `Shared/Views/CustomizeView/SubViews/IconPickerView.swift`:89 | `.fill().foregroundColor()` incorrect modifier order — wrong selection color |
|
||
| WARNING | `Shared/Utilities/AccessibilityHelpers.swift`:74 | `.accessibilityHint("")` applied with empty string — VoiceOver may announce blank hint |
|
||
| WARNING | `Shared/Utilities/AccessibilityHelpers.swift`:33 | `value: UUID()` on animation triggers on every render |
|
||
| WARNING | `Shared/Utilities/AccessibilityHelpers.swift`:13 | Custom `\.reduceMotion` env key defined but never read |
|
||
| WARNING | `Shared/Views/SharingTemplates/Variations/CurrentStreakVariations.swift`:210 | "Last 10 Days" hardcoded, inaccurate when fewer entries |
|
||
| WARNING | `Shared/Views/AddMoodHeaderView.swift`:700 | `NeonBarButtonStyle` ignores `accessibilityReduceMotion` |
|
||
| WARNING | `Shared/Models/MoodTintable.swift`:78 | All 5 custom colors default to same `#a92b26` — indistinguishable for color-blind users |
|
||
|
||
|
||
---
|
||
|
||
## Energy/Battery Auditor (23 findings)
|
||
|
||
| Severity | File | Finding |
|
||
|----------|------|---------|
|
||
| CRITICAL | `Shared/Views/EntryListView.swift`:2224 | `CMMotionManager` started at 30 Hz in `.onAppear`, `stop()` never called — continuous battery drain |
|
||
| CRITICAL | `Shared/DemoAnimationManager.swift`:91 | 60 Hz Timer spawns new `Task` on every tick — up to 18,000 Task allocations over animation lifetime |
|
||
| CRITICAL | `Shared/BGTask.swift`:14 | Background task annotated `@MainActor` — Core Data fill runs on main thread, defeats background intent |
|
||
| CRITICAL | `Shared/Views/SettingsView/LiveActivityPreviewView.swift`:325 | `ImageRenderer` called on `DispatchQueue.global` — threading violation, corrupt images |
|
||
| BUG | `Shared/DemoAnimationManager.swift`:54 | `DispatchQueue.main.asyncAfter` callbacks not cancellable — multiple timers accumulate on rapid restarts |
|
||
| BUG | `Shared/MoodStreakActivity.swift`:215 | Infinite loop if `Calendar.date(byAdding:)` returns nil — main actor hang |
|
||
| BUG | `Shared/FeelsApp.swift`:92 | `Task.detached { @MainActor in }` defeats detached — all Core Data work on main thread |
|
||
| BUG | `Shared/Views/LockScreenView.swift`:1711 | `try? Task.sleep` swallows `CancellationError` — auth prompt after view disappears |
|
||
| BUG | `Shared/Views/CelebrationAnimations.swift`:109 | `DispatchQueue.asyncAfter` fires mood-save after view dismissal — potential duplicate entries |
|
||
| BUG | `Shared/Views/SettingsView/LiveActivityPreviewView.swift`:89 | `animationTimer` not nil'd after invalidation — dangling timer reference |
|
||
| BUG | `Shared/Services/PhotoManager.swift`:104 | Synchronous disk I/O on `@MainActor` — blocks scroll frame budget on photo loads |
|
||
| BUG | `Shared/Views/FeelsSubscriptionStoreView.swift`:51 | `dismiss()` called after async op in stale Task — paywall may not dismiss after purchase |
|
||
| PERFORMANCE | `Shared/MoodStreakActivity.swift`:306 | Two long-duration Timers scheduled without `.common` run-loop mode — freeze during scroll |
|
||
| PERFORMANCE | `Shared/Views/EntryListView.swift`:2249 | Motion callbacks on `.main` queue with `withAnimation` at 30 Hz — 30 animation transactions/sec |
|
||
| PERFORMANCE | `Shared/Views/InsightsView/InsightsViewModel.swift`:88 | `withTaskGroup` children all `@MainActor` — zero actual concurrency, 3× slower |
|
||
| PERFORMANCE | `Shared/Views/InsightsView/InsightsViewModel.swift`:60 | `generateInsights()` spawns new Task on every call — concurrent stale LLM requests |
|
||
| PERFORMANCE | `Shared/Views/FeelsSubscriptionStoreView.swift`:164 | `repeatForever` animations not cancelled on dismiss — GPU work during teardown |
|
||
| PERFORMANCE | `Shared/Views/AddMoodHeaderView.swift`:343 | `OrbitVotingView`/`NeonVotingView` `repeatForever` pulse — conflicting states on rapid layout switches |
|
||
| PERFORMANCE | `FeelsWidget2/WidgetProviders.swift`:127 | Widget reload policy `now+10s`, first entry dated `now+15s` — unnecessary 5-second early reload |
|
||
| PERFORMANCE | `FeelsWidget2/FeelsTimelineWidget.swift`:73 | `DateFormatter` computed property — new allocation on every widget render (2–4 per render) |
|
||
| PERFORMANCE | `FeelsWidget2/FeelsVoteWidget.swift`:68 | `DateFormatter` and multi-UserDefaults JSON decodes per widget render |
|
||
| PERFORMANCE | `Shared/Utilities/AccessibilityHelpers.swift`:33 | `value: UUID()` on `.animation` triggers animation on every render pass |
|
||
| PERFORMANCE | `Shared/Views/YearView/YearViewModel.swift`:44 | `filterEntries` double-clear causes two empty-state flashes + synchronous Core Data on main thread |
|
||
|
||
|
||
---
|
||
|
||
## Storage Auditor (28 findings)
|
||
|
||
| Severity | File | Finding |
|
||
|----------|------|---------|
|
||
| CRITICAL | `Shared/Services/PhotoManager.swift`:26 | Photos stored in App Group container root without `.isExcludedFromBackupKey` — double-backed up to iCloud |
|
||
| CRITICAL | `Shared/Persisence/SharedModelContainer.swift`:86 | SwiftData store at App Group container root — wrong location, not guaranteed preserved on migration |
|
||
| CRITICAL | `Shared/Persisence/ExtensionDataProvider.swift`:91 | Widget opens same SQLite store as main app with `cloudKitDatabase: .none` — incompatible CloudKit configs |
|
||
| CRITICAL | `Shared/SharedMoodIntent.swift`:93 | New `ModelContainer` per widget intent invocation — concurrent writers, WAL contention |
|
||
| CRITICAL | `Shared/Random.swift`:28 | Force-unwrap `UserDefaults(suiteName:)!` — crash if App Group entitlement unavailable |
|
||
| CRITICAL | `Shared/Models/UserDefaultsStore.swift`:226 | App Group UserDefaults unprotected — `hasActiveSubscription`, `privacyLockEnabled` readable while locked |
|
||
| CRITICAL | `Shared/Persisence/DataControllerADD.swift`:13 | Non-atomic delete-save-insert — data loss if second save fails |
|
||
| CRITICAL | `Shared/Persisence/ExtensionDataProvider.swift`:204 | Same non-atomic delete-save-insert in widget extension — higher failure risk in memory-constrained process |
|
||
| CRITICAL | `Shared/Analytics.swift`:24 | PostHog API key hardcoded in source — permanently in git history |
|
||
| CRITICAL | `Shared/Persisence/DataController.swift`:15 | `DataController.shared` static let on `@MainActor` class — init can run off-main-actor |
|
||
| BUG | `Shared/Services/PhotoManager.swift`:34 | Photos directory at App Group container root — may be lost during OS migration |
|
||
| BUG | `Shared/Services/PhotoManager.swift`:95 | Thumbnail write errors silently swallowed; thumbnails not excluded from backup |
|
||
| BUG | `Shared/Persisence/SharedModelContainer.swift`:66 | Silent fallback to in-memory storage when App Group unavailable — all data ephemeral |
|
||
| BUG | `Shared/IAPManager.swift`:68 | `firstLaunchDate` falls back to `Date()` on every cold launch — trial clock resets |
|
||
| BUG | `Shared/IAPManager.swift`:206 | Offline subscription fallback reads unprotected App Group plist — editable by attacker |
|
||
| BUG | `Shared/Services/BiometricAuthManager.swift`:17 | `isUnlocked` defaults to `true` — content briefly visible before authentication completes |
|
||
| BUG | `Shared/Views/SettingsView/SettingsView.swift`:1158 | `URL(string: "shareddocuments://\(url.path)")!` — crashes on paths with special characters |
|
||
| BUG | `Shared/Services/ImageCache.swift`:24 | `NotificationCenter.addObserver` token leaked — observer never removed |
|
||
| WARNING | `Shared/Services/ExportService.swift`:80 | Export files written to `temporaryDirectory` without cleanup — orphaned files accumulate |
|
||
| WARNING | `Shared/Services/ExportService.swift`:171 | `temporaryDirectory` file may be purged before share sheet completes |
|
||
| WARNING | `Shared/Models/UserDefaultsStore.swift`:226 | All prefs including sensitive values stored in shared App Group defaults — too broad surface |
|
||
| WARNING | `Shared/Models/UserDefaultsStore.swift`:217 | Static `cachedOnboardingData` with no thread synchronization — data race |
|
||
| WARNING | `Shared/Models/UserDefaultsStore.swift`:309 | Deprecated `UserDefaults.synchronize()` called — no-op since iOS 12 |
|
||
| WARNING | `Shared/Models/OnboardingDataDataManager.swift`:27 | Second `synchronize()` call site |
|
||
| WARNING | `Shared/Services/ReviewRequestManager.swift`:74 | Review state in `UserDefaults.standard` — moods logged via widget never increment counter |
|
||
| WARNING | `Shared/Analytics.swift`:28 | Analytics opt-out state in UserDefaults — no file protection level enforcement |
|
||
| WARNING | `Shared/Services/PhotoManager.swift`:187 | `UIGraphicsBeginImageContextWithOptions` deprecated in iOS 17 |
|
||
| WARNING | `FeelsWidget2/FeelsTimelineWidget.swift`:236 | Widget fetches from Unix epoch — loads entire history into 30 MB widget extension memory limit |
|
||
|
||
|
||
---
|
||
|
||
## iCloud/CloudKit Auditor (22 findings)
|
||
|
||
| Severity | File | Finding |
|
||
|----------|------|---------|
|
||
| CRITICAL | `Shared/Persisence/SharedModelContainer.swift`:34 | Main app and widget write to same SQLite file with different CloudKit configs — unsupported, can corrupt store |
|
||
| CRITICAL | `Shared/SharedMoodIntent.swift`:93 | New `ModelContainer` per widget intent — concurrent SQLite writers, WAL uncommitted frames |
|
||
| CRITICAL | `Shared/SharedMoodIntent.swift`:109 | Non-atomic delete-save-insert — CloudKit can sync delete before insert arrives on other device |
|
||
| CRITICAL | `Shared/Persisence/DataControllerADD.swift`:19 | Delete-save-insert non-atomic — CloudKit push between two saves causes remote device to lose entry |
|
||
| CRITICAL | `Shared/Persisence/ExtensionDataProvider.swift`:198 | Watch extension same non-atomic pattern with CloudKit enabled — data loss during poor connectivity |
|
||
| CRITICAL | `Shared/Persisence/DataControllerGET.swift`:74 | Infinite loop in `calculateStreak` — main actor deadlock from nil Calendar result |
|
||
| BUG | `Shared/Persisence/DataController.swift`:78 | `refreshFromDisk()` calls `modelContext.rollback()` — discards pending CloudKit-delivered changes |
|
||
| BUG | `Shared/FeelsApp.swift`:92 | `Task.detached { @MainActor in }` runs Core Data work on main thread — blocks during CloudKit merges |
|
||
| BUG | `Shared/Persisence/ExtensionDataProvider.swift`:61 | Widget opens CloudKit-managed store as local-only — corrupts `_cloudkit_metadata` table |
|
||
| BUG | `Shared/Persisence/DataController.swift`:15 | Static `DataController.shared` on `@MainActor` — init can run on wrong thread |
|
||
| BUG | `Shared/Persisence/DataController.swift`:55 | `saveAndRunDataListeners()` runs listeners even when `save()` fails — UI shows unsaved changes |
|
||
| BUG | `Shared/Persisence/DataController.swift`:51 | `addNewDataListener` grows unboundedly — stale closures execute on every CloudKit refresh |
|
||
| BUG | `Shared/BGTask.swift`:22 | `@MainActor` BGTask marks complete before async `processPendingSideEffects()` finishes |
|
||
| BUG | `Shared/BGTask.swift`:36 | Force-unwrap on Calendar date in BGTask — crashes background scheduler |
|
||
| BUG | `Shared/Persisence/SharedModelContainer.swift`:66 | Silent fallback to in-memory on App Group unavailability — CloudKit sync never runs |
|
||
| WARNING | `FeelsWidgetExtension.entitlements`:1 | Widget declares CloudKit entitlement it must never use — potential App Store rejection |
|
||
| WARNING | `FeelsWidgetExtension.entitlements`:1 | Wrong entitlement file selection silently falls back to in-memory storage |
|
||
| WARNING | `Shared/Persisence/ExtensionDataProvider.swift`:84 | Debug/release group mismatch can cause nil container URL — in-memory fallback |
|
||
| WARNING | `Shared/Models/MoodEntryModel.swift`:22 | No versioned SwiftData schema migration — enum reordering silently corrupts all CloudKit records |
|
||
| WARNING | `Shared/Persisence/DataControllerADD.swift`:46 | `+12 hours` timezone hack in `fillInMissingDates` — dates may land on wrong day after CloudKit sync |
|
||
| WARNING | `Shared/Persisence/DataControllerGET.swift`:18 | `entry.forDate <= endDate` inclusive bound — entries at exact midnight of next day double-counted |
|
||
| WARNING | `Shared/Models/UserDefaultsStore.swift`:217 | `cachedOnboardingData` data race — wrong dates used for gap-filling, synced to CloudKit |
|
||
|
||
|
||
---
|
||
|
||
## SwiftUI Architecture Auditor (49 findings — selected)
|
||
|
||
| Severity | File | Finding |
|
||
|----------|------|---------|
|
||
| CRITICAL | `Shared/Views/SettingsView/SettingsTabView.swift`:62 | `CustomizeContentView` missing `iapManager`/`authManager` environment objects — crash on Customize tab |
|
||
| CRITICAL | `Shared/Views/MonthView/MonthView.swift`:29 | Singleton `OnboardingDataDataManager.shared` wrapped in `@StateObject` — SwiftUI lifecycle violated |
|
||
| CRITICAL | `Shared/Views/MonthView/MonthView.swift`:49 | Singleton `DemoAnimationManager.shared` in `@StateObject` — conflicting owner semantics |
|
||
| CRITICAL | `Shared/Views/CustomizeView/SubViews/DayFilterPickerView.swift`:12 | Singleton `DaysFilterClass.shared` in `@StateObject` — changes may not propagate |
|
||
| CRITICAL | `Shared/Persisence/DataControllerGET.swift`:74 | Infinite loop in `calculateStreak` — `?? checkDate` nil-coalescing never advances |
|
||
| CRITICAL | `Shared/MoodStreakActivity.swift`:215 | Identical infinite loop in `LiveActivityScheduler.calculateStreak` |
|
||
| CRITICAL | `Shared/AppShortcuts.swift`:136 | `while true` streak loop on `@MainActor` — thousands of Core Data fetches for long-streak users |
|
||
| CRITICAL | `Shared/Views/MoodEntryFunctions.swift`:25 | `newGrouped[year] = newMonth` inside inner loop — all months but last dropped per year |
|
||
| BUG | `Shared/Views/DayView/DayViewViewModel.swift`:29 | Double force-unwrap `year![$0]!.count` in `numberOfEntries` |
|
||
| BUG | `Shared/Views/DayView/DayViewViewModel.swift`:115 | Force-unwrap on `DateComponents.month!`/`.year!` |
|
||
| BUG | `Shared/Views/YearView/YearViewModel.swift`:27 | `updateData()` only sets dates, never populates `data` or `entriesByYear` — misleading name |
|
||
| BUG | `Shared/Views/YearView/YearViewModel.swift`:48 | Double `removeAll()` causes two empty-state re-renders with empty dict |
|
||
| BUG | `Shared/Views/YearView/YearView.swift`:334 | `YearCard.Equatable` compares only `entries.count` — same count with different moods prevents re-render |
|
||
| BUG | `Shared/Views/MonthView/MonthView.swift`:413 | `MonthCard.Equatable` excludes `demoManager` — demo animations silently stop |
|
||
| BUG | `Shared/Views/MonthView/MonthView.swift`:176 | `ForEach id: \.element.month` collides across years — SwiftUI drops or misidentifies month cards |
|
||
| BUG | `Shared/Views/InsightsView/InsightsViewModel.swift`:59 | Unstructured task on `generateInsights()` — race condition with concurrent task mutations |
|
||
| BUG | `Shared/Views/InsightsView/InsightsViewModel.swift`:77 | Force-unwrap on Calendar `dateComponents` — crash on non-Gregorian calendars |
|
||
| BUG | `Shared/Views/Views/PersonalityPackPickerView.swift`:52 | `.alert` inside `ForEach` — only last row's alert fires |
|
||
| BUG | `Shared/Views/CustomizeView/SubViews/DayFilterPickerView.swift`:21 | `addFilter`/`removeFilter` are no-ops — buttons do nothing silently |
|
||
| BUG | `Shared/Views/CelebrationAnimations.swift`:109 | `DispatchQueue.asyncAfter` mood-save fires after dismissal — ghost/duplicate entries |
|
||
| BUG | `Shared/BGTask.swift`:42 | Error interpolation typo `(error)` — scheduling failures invisible |
|
||
| BUG | `Shared/BGTask.swift`:14 | `@MainActor` BGTask runs Core Data fill on main thread |
|
||
| BUG | `Shared/Models/MoodEntryFunctions.swift`:25 | Inner loop overwrites `newGrouped[year]` — confirmed data display bug for multi-year users |
|
||
| BUG | `Shared/Views/AddMoodHeaderView.swift`:20 | `@State var onboardingData` captures value once — stale header after Settings changes |
|
||
| BUG | `Shared/Onboarding/views/OnboardingMain.swift`:12 | Reference-type `OnboardingData` wrapped in `@State` — changes not observed |
|
||
| BUG | `Shared/Onboarding/views/OnboardingSubscription.swift`:139 | Onboarding completion fires on any sheet dismiss — including cancel |
|
||
| BUG | `Shared/Models/BGView.swift`:76 | `BGView.Equatable ==` hardcoded to `true` — view never re-renders on prop changes |
|
||
| BUG | `Shared/Models/BGView.swift`:52 | `randomMood` computed — new random value on every render, background flickers |
|
||
| BUG | `Shared/Random.swift`:65 | `monthSymbols[fromMonthInt-1]` — out-of-bounds crash when `fromMonthInt == 0` |
|
||
| BUG | `Shared/Views/SharingTemplates/LongestStreakTemplate.swift`:46 | Non-`@MainActor` `init` calls `@MainActor` `configureData()` — Swift 6 violation, no-op writes |
|
||
| BUG | `Shared/Services/LiveActivityPreviewView.swift`:325 | `ImageRenderer` on background thread — corrupt images |
|
||
| WARNING | `Shared/Views/DayView/DayView.swift`:17 | Dead `@AppStorage deleteEnabled` — every UserDefaults write triggers needless body re-evaluation |
|
||
| WARNING | `Shared/Views/DayView/DayView.swift`:32 | Dead `@State showTodayInput` |
|
||
| WARNING | `Shared/Views/YearView/YearView.swift`:13 | Dead `@State toggle = true` |
|
||
| WARNING | `Shared/Views/MonthView/MonthDetailView.swift`:21 | Duplicate `showingUpdateEntryAlert`/`showUpdateEntryAlert` booleans — dead state |
|
||
| WARNING | `Shared/Views/EntryListView.swift`:2073 | `@ObservedObject` on singleton in struct — hundreds of observation points |
|
||
| WARNING | `Shared/Views/InsightsView/InsightsViewModel.swift`:88 | `withTaskGroup` children all `@MainActor` — serialized, no concurrency benefit |
|
||
| WARNING | `Shared/Views/CustomizeView/CustomizeView.swift`:295 | Hidden `Text` hack to force re-renders — fragile, may be optimized away by Apple |
|
||
| WARNING | `Shared/Views/MonthView/MonthView.swift`:342 | Two `.onAppear` modifiers on same view |
|
||
| WARNING | `Shared/Views/SettingsView/SettingsView.swift`:94 | `OnboardingMain` sheet missing `iapManager` environment object injection |
|
||
| WARNING | `Shared/IAPManager.swift`:289 | `try?` on subscription status silently grants premium on StoreKit errors |
|
||
| WARNING | `Shared/IAPManager.swift`:40 | `#if DEBUG` bypass block is dead code — both branches are `false` |
|
||
| WARNING | `Shared/Services/WatchConnectivityManager.swift`:117 | `pendingMoods` data race — WCSession delegate vs main thread |
|
||
| WARNING | `Shared/Services/PhotoManager.swift`:104 | Synchronous disk I/O on `@MainActor` — main thread blocked during photo loads |
|
||
| WARNING | `Shared/Services/ImageCache.swift`:24 | `NotificationCenter` observer token leaked |
|
||
| WARNING | `Shared/Views/SettingsView/SettingsTabView.swift`:157 | `NavigationView` deprecated — iPad shows unwanted split layout |
|
||
| WARNING | `Shared/Utilities/AccessibilityHelpers.swift`:33 | `value: UUID()` triggers animation on every render |
|
||
| WARNING | `Shared/Onboarding/views/OnboardingTime.swift`:13 | `DateFormatter` computed property re-allocates on every DatePicker tick |
|
||
| WARNING | `Shared/Views/NoteEditorView.swift`:474 | Force-unwrap `entry.photoID!` despite `if let` guard present |
|
||
|
||
|
||
---
|
||
|
||
## Swift Performance Auditor (31 findings)
|
||
|
||
| Severity | File | Finding |
|
||
|----------|------|---------|
|
||
| CRITICAL | `Shared/AppShortcuts.swift`:128 | `while true` on `@MainActor` — one Core Data fetch per day, up to 1000+ for long streaks |
|
||
| CRITICAL | `Shared/Persisence/DataControllerGET.swift`:74 | Infinite loop if `calendar.date(byAdding:)` returns nil in streak calculation |
|
||
| CRITICAL | `Shared/MoodStreakActivity.swift`:215 | Same infinite loop in `LiveActivityScheduler.calculateStreak` |
|
||
| CRITICAL | `FeelsWidget2/FeelsTimelineWidget.swift`:236 | Widget fetches from Unix epoch — entire history in memory-constrained extension process |
|
||
| PERFORMANCE | `Shared/Views/YearView/YearViewModel.swift`:36 | O(n log n) sort to find minimum date — use `min(by:)` instead |
|
||
| PERFORMANCE | `Shared/Views/YearView/YearViewModel.swift`:48 | Double `removeAll()` on `@Published` dicts — two re-renders with empty state |
|
||
| PERFORMANCE | `Shared/Views/YearView/YearViewModel.swift`:44 | Synchronous Core Data + O(n) grid build on `@MainActor` on every filter change |
|
||
| PERFORMANCE | `Shared/Random.swift`:91 | `createTotalPerc` runs 5 separate O(n) filters — use single-pass histogram |
|
||
| PERFORMANCE | `Shared/Views/InsightsView/InsightsViewModels/MoodDataSummarizer.swift`:213 | Two full O(n log n) sorts in one function call on already-sorted input |
|
||
| PERFORMANCE | `Shared/Views/InsightsView/InsightsViewModels/MoodDataSummarizer.swift`:265 | Redundant sort inside `calculateMoodStreak` called on already-sorted input |
|
||
| PERFORMANCE | `Shared/Models/MoodTintable.swift`:109 | `getCustomMoodTint()` called 10 times per render — each call decodes JSON from UserDefaults |
|
||
| PERFORMANCE | `Shared/Views/ChartDataBuildable.swift`:31 | String interpolation for dict key inside inner loop — 1000 String allocs for 1000 entries |
|
||
| PERFORMANCE | `Shared/MoodLogger.swift`:134 | `ISO8601DateFormatter()` allocated fresh on every `markSideEffectsApplied`/`sideEffectsApplied` |
|
||
| PERFORMANCE | `FeelsWidget2/FeelsTimelineWidget.swift`:73 | `DateFormatter` as computed property — new allocation on every widget render (×3 sizes) |
|
||
| PERFORMANCE | `FeelsWidget2/FeelsVoteWidget.swift`:68 | `DateFormatter` inside computed `votingDateString` + multiple UserDefaults decodes per render |
|
||
| PERFORMANCE | `Shared/Views/DemoAnimationManager.swift`:91 | 60 Hz Timer spawning new `Task` per tick — tasks queue up under load |
|
||
| PERFORMANCE | `Shared/Utilities/AccessibilityHelpers.swift`:33 | `value: UUID()` triggers animation on every render pass |
|
||
| PERFORMANCE | `Shared/Onboarding/views/OnboardingTime.swift`:13 | `DateFormatter` computed on DatePicker view — new allocation every ~16ms during scroll |
|
||
| PERFORMANCE | `Shared/Views/Views/ExportView.swift`:177 | `DateFormatter` re-created on every `dateRangeText` access |
|
||
| PERFORMANCE | `Shared/Views/CustomizeView/SubViews/ImagePackPickerView.swift`:50 | O(n log n) sort inside `ForEach` per row to find max rawValue — use `max(by:)` |
|
||
| PERFORMANCE | `Shared/Views/CustomizeView/SubViews/PersonalityPackPickerView.swift`:64 | Same sorted-allCases anti-pattern |
|
||
| PERFORMANCE | `Shared/Views/InsightsView/InsightsViewModels/MoodDataSummarizer.swift`:158 | `weekdayNames[weekday - 1]` — no bounds check, crashes on weekDay == 0 |
|
||
| PERFORMANCE | `Shared/Random.swift`:28 | `groupDefaults` computed static var — `UserDefaults(suiteName:)` called on every access |
|
||
| PERFORMANCE | `Shared/Views/DateFormattingCache.swift`:263 | `DateFormattingCache.shared` accessed from multiple threads with no synchronization |
|
||
| PERFORMANCE | `Shared/Views/BGView.swift`:52 | `randomMood` computed — new random value every render, all 40–100 background cells re-randomize |
|
||
| PERFORMANCE | `Shared/Views/SettingsView/SettingsView.swift`:1158 | Force-unwrap URL from runtime path — crash if path has special characters |
|
||
| PERFORMANCE | `Shared/Random.swift`:48 | `existingWeekdayName`/`existingDayFormat` mutable static dicts — no thread synchronization |
|
||
| PERFORMANCE | `Shared/Random.swift`:174 | `textToImageCache` static dict — no thread synchronization, concurrent widget + app rendering |
|
||
| PERFORMANCE | `Shared/Services/HealthService.swift`:440 | 365 concurrent HealthKit task group tasks — floods cooperative thread pool |
|
||
| PERFORMANCE | `Shared/Persisence/DataControllerGET.swift`:89 | `splitIntoYearMonth` fetches from epoch — loads entire history into memory |
|
||
| PERFORMANCE | `Shared/Views/InsightsView/InsightsViewModel.swift`:88 | `withTaskGroup` child tasks all `@MainActor` — LLM calls run serially not concurrently |
|
||
|
||
|
||
---
|
||
|
||
## Modernization Auditor (43 findings — selected)
|
||
|
||
| Severity | File | Finding |
|
||
|----------|------|---------|
|
||
| CRITICAL | `Shared/Analytics.swift`:24 | PostHog API key hardcoded in source — permanently in git history |
|
||
| CRITICAL | `Shared/Views/SettingsView/SettingsTabView.swift`:62 | `CustomizeContentView` missing environment objects — crash |
|
||
| CRITICAL | `Shared/Views/CelebrationAnimations.swift`:109 | `DispatchQueue.asyncAfter` mood-save fires after dismissal — duplicate entries |
|
||
| CRITICAL | `Shared/MoodStreakActivity.swift`:215 | Infinite loop in streak calculation |
|
||
| CRITICAL | `Shared/Persisence/DataControllerGET.swift`:74 | Infinite loop in streak calculation |
|
||
| CRITICAL | `Shared/AppShortcuts.swift`:136 | `while true` streak loop on main actor |
|
||
| CRITICAL | `Shared/Views/MoodEntryFunctions.swift`:25 | Inner loop overwrites year grouping — all months but last lost |
|
||
| CRITICAL | `Shared/Views/MonthView/MonthView.swift`:176 | `ForEach id: \.month` ID collision across years |
|
||
| BUG | `Shared/IAPManager.swift`:289 | `try?` grants premium on StoreKit errors |
|
||
| BUG | `Shared/BGTask.swift`:42 | `(error)` typo — scheduling failures invisible |
|
||
| BUG | `Shared/BGTask.swift`:14 | `@MainActor` BGTask runs Core Data on main thread |
|
||
| BUG | `Shared/FeelsApp.swift`:92 | `Task.detached { @MainActor in }` contradictory pattern |
|
||
| BUG | `Shared/Services/HealthService.swift`:79 | `isAuthorized = true` unconditional — HealthKit denial treated as success |
|
||
| BUG | `Shared/Views/PhotoPickerView.swift`:151 | `@State` mutation from non-`@MainActor` async context |
|
||
| BUG | `Shared/Views/SettingsView/SettingsView.swift`:1158 | Force-unwrap URL with runtime path |
|
||
| BUG | `Shared/Views/SharingTemplates/LongestStreakTemplate.swift`:52 | Non-`@MainActor` init calling `@MainActor` function |
|
||
| BUG | `Shared/Onboarding/views/OnboardingSubscription.swift`:139 | Onboarding completion unconditional on dismiss |
|
||
| BUG | `Shared/Services/WatchConnectivityManager.swift`:117 | `pendingMoods` data race |
|
||
| WARNING | `Shared/Services/PhotoManager.swift`:187 | `UIGraphicsBeginImageContextWithOptions` deprecated iOS 17 |
|
||
| WARNING | `Shared/Random.swift`:187 | Second `UIGraphicsBeginImageContext` deprecated call site |
|
||
| WARNING | `Shared/Models/UserDefaultsStore.swift`:309 | `synchronize()` deprecated no-op — 3 call sites |
|
||
| WARNING | `Shared/FeelsTips.swift`:257 | `DispatchQueue.main.asyncAfter` inside `.onAppear` — should use `.task { try? await Task.sleep }` |
|
||
| WARNING | `Shared/Views/NoteEditorView.swift`:84 | `DispatchQueue.asyncAfter` for `@FocusState` — fixed in SwiftUI 5.0+ |
|
||
| WARNING | `Shared/Views/SettingsView/LiveActivityPreviewView.swift`:325 | `ImageRenderer` on background thread |
|
||
| WARNING | `Shared/Views/InsightsView/InsightsView.swift`:97 | `refreshInsights()` called without `await` — spinner dismisses before completion |
|
||
| WARNING | `Shared/Views/InsightsView/InsightsViewModel.swift`:88 | `withTaskGroup` child tasks serialized by `@MainActor` |
|
||
| WARNING | `Shared/Views/EntryListView.swift`:2242 | `CMMotionManager` never stopped |
|
||
| WARNING | Multiple files | 8 files use deprecated `@Environment(\.presentationMode)` — replace with `@Environment(\.dismiss)` |
|
||
| WARNING | Multiple files (10) | `.edgesIgnoringSafeArea(.all)` deprecated — replace with `.ignoresSafeArea()` |
|
||
| WARNING | `Shared/Models/Theme.swift`:74 | `Themeable` protocol mandates `AnyView` return types — prevents compiler optimization |
|
||
| WARNING | `Shared/Models/Shapes.swift`:17 | `AnyView` in `BGShape.view()` — defeats SwiftUI structural diffing |
|
||
| WARNING | `Shared/Views/SwitchableView.swift`:301 | `AnyView` for all switchable view cases |
|
||
| WARNING | `Shared/Services/HealthService.swift`:154 | `withCheckedContinuation` wrapping HKQuery — errors silently dropped as values |
|
||
| WARNING | `Shared/Analytics.swift`:615 | `@MainActor` method called from non-isolated ViewModifier closure — Swift 6 error |
|
||
| WARNING | `Shared/Views/EntryListView.swift`:119 | `.foregroundColor()` soft-deprecated iOS 17 — 719 occurrences across 66 files |
|
||
| WARNING | 47 files | `PreviewProvider` pattern — replace with `#Preview` macro for faster previews |
|
||
| WARNING | `Shared/Views/DemoAnimationManager.swift`:91 | Timer + Task wrapping on `@MainActor` — call `updateProgress()` directly |
|
||
| WARNING | `Shared/Views/YearView/YearViewModel.swift`:11 | `ObservableObject` candidate for `@Observable` migration |
|
||
| WARNING | `Shared/Views/DayView/DayViewViewModel.swift`:13 | `ObservableObject` candidate for `@Observable` migration |
|
||
| WARNING | `Shared/Onboarding/OnboardingData.swift`:13 | `@Published` mutations need `@MainActor` protection |
|
||
| WARNING | `Shared/Views/MonthView/MonthView.swift`:31 | Nested `ObservableObject` class without `@MainActor` |
|
||
| WARNING | `Shared/Onboarding/views/OnboardingMain.swift`:12 | Reference-type class wrapped in `@State` — should be `@StateObject` |
|
||
| WARNING | `Shared/Models/MoodTintable.swift`:78 | `SavedMoodTint` has unnecessary `NSObject` inheritance — prevents `@Observable` migration |
|
||
|
||
|
||
---
|
||
|
||
## Navigation Auditor (27 findings)
|
||
|
||
| Severity | File | Finding |
|
||
|----------|------|---------|
|
||
| CRITICAL | `Shared/Views/SettingsView/SettingsTabView.swift`:62 | `CustomizeContentView` missing `iapManager` environment object — crash on Customize tab |
|
||
| CRITICAL | `Shared/Views/CustomizeView/SubViews/AppThemePickerView.swift`:262 | `theme.previewColors[0]`/`[1]` unchecked subscript — crash if fewer than 2 colors |
|
||
| CRITICAL | `Shared/Views/Sharing/SharingStylePickerView.swift`:206 | `designs[selectedIndex]` no bounds check in `LongestStreakPickerView` — crash when designs empty |
|
||
| BUG | `Shared/FeelsApp.swift`:30 | Force cast `task as! BGProcessingTask` — crash if system delivers different task type |
|
||
| BUG | `Shared/Views/MainTabView.swift`:53 | Onboarding sheet empty `onDismiss` — `needsOnboarding` never set false if system dismisses |
|
||
| BUG | `Shared/Views/DayView/DayView.swift`:48 | `.sheet(item: $selectedEntry)` — stale entry reference on concurrent data update |
|
||
| BUG | `Shared/Views/MonthView/MonthView.swift`:176 | `ForEach id: \.element.month` — duplicate IDs across years, months silently dropped |
|
||
| BUG | `Shared/Views/MonthView/MonthView.swift`:339 | `showSubscriptionStore` sheet missing `iapManager` environment object — crash on month gate |
|
||
| BUG | `Shared/Views/FeelsSubscriptionStoreView.swift`:47 | `dismiss()` in async Task after status check — stale environment, potential double-dismiss |
|
||
| BUG | `Shared/Views/SettingsView/SettingsView.swift`:94 | `OnboardingMain` sheet missing `iapManager` — crash on "Show Onboarding" in Settings |
|
||
| BUG | `Shared/Views/SettingsView/SettingsView.swift`:1158 | Force-unwrap URL from runtime path — crash on special characters |
|
||
| BUG | `Shared/Views/CelebrationAnimations.swift`:109 | Un-cancellable `asyncAfter` mood-save fires after navigation away — duplicate entries |
|
||
| BUG | `Shared/Views/CustomizeView/SubViews/AppThemePickerView.swift`:117 | Double-dismiss: `selectedTheme = nil` dismisses sheet, then explicit `dismiss()` also called |
|
||
| BUG | `Shared/Onboarding/views/OnboardingSubscription.swift`:139 | Onboarding completion unconditional on sheet dismiss |
|
||
| BUG | `Shared/Views/CustomizeView/CustomizeView.swift`:529 | `as? CustomWidgetModel` cast on `NSCopying.copy()` — nil result presents blank sheet |
|
||
| BUG | `Shared/Views/DayView/DayViewViewModel.swift`:29 | Double force-unwrap in `numberOfEntries` |
|
||
| BUG | `Shared/Views/Views/EntryListView.swift` (multiple) | Multiple `.sheet` modifiers on same view — only one reliably presents on iOS 16.4 and earlier |
|
||
| WARNING | `Shared/Views/DayView/DayView.swift`:46 | `showingSheet` boolean bound to `SettingsView` — never triggered, permanently unreachable dead sheet |
|
||
| WARNING | `Shared/Views/MainTabView.swift`:18 | `onboardingData` captured as non-reactive `let` — stale after CloudKit sync |
|
||
| WARNING | `Shared/Views/MainTabView.swift`:79 | `UIApplication.shared.connectedScenes.first` — wrong scene on iPad multi-window |
|
||
| WARNING | `Shared/Onboarding/views/OnboardingMain.swift`:18 | TabView pager with no navigation guards — user can swipe to completion without filling fields |
|
||
| WARNING | `Shared/Views/MonthView/MonthView.swift`:350 | Sheet item driven by optional `selectedDetail.selectedItem` — nil content if race between item and show |
|
||
| WARNING | `Shared/Views/MonthView/MonthDetailView.swift`:85 | Multiple rapid taps on share enqueue multiple `showSheet = true` — stale image in sheet |
|
||
| WARNING | `Shared/Views/InsightsView/InsightsView.swift`:97 | `refreshInsights()` not awaited — spinner dismisses before load completes |
|
||
| WARNING | `Shared/Views/InsightsView/InsightsViewModel.swift`:60 | `generateInsights()` no deduplication — concurrent tasks interleave published state |
|
||
| WARNING | `Shared/Onboarding/views/OnboardingStyle.swift`:70 | `theme.apply()` called immediately on every tap — no rollback if onboarding abandoned |
|
||
| WARNING | `Shared/Views/NoteEditorView.swift`:32 | Inner `NavigationStack` inside modal sheet — orphaned from app navigation hierarchy |
|
||
|
||
|
||
---
|
||
|
||
## Testing Auditor (47 findings — selected)
|
||
|
||
| Severity | File | Finding |
|
||
|----------|------|---------|
|
||
| CRITICAL | `Tests iOS/Tests_iOS.swift`:1 | Only 2 active unit test methods — cover one utility function; entire business logic layer untested |
|
||
| CRITICAL | `Tests iOS/Tests_iOS.swift`:1 | Zero Swift Testing (`@Test`/`#expect`) migration — XCTest boilerplate prevents async actor testing |
|
||
| CRITICAL | `Shared/Persisence/DataController.swift`:15 | `DataController.shared` singleton direct-access in 10+ views — untestable by design |
|
||
| CRITICAL | `Shared/Persisence/DataControllerProtocol.swift`:1 | `DataControlling` protocol exists but never used as DI type — dead testability infrastructure |
|
||
| CRITICAL | `Shared/MoodLogger.swift`:16 | `MoodLogger` wires 8 singleton side effects directly — impossible to unit test logging path |
|
||
| CRITICAL | `Shared/Persisence/DataControllerGET.swift`:74 | Infinite loop in `calculateStreak` — untestable without mock/injectable calendar |
|
||
| CRITICAL | `Shared/Persisence/DataControllerADD.swift`:13 | Non-atomic delete-save-insert — data loss untestable without mock context |
|
||
| CRITICAL | `Shared/AppShortcuts.swift`:128 | `while true` streak loop — O(n) Core Data per iteration, untestable without mock |
|
||
| CRITICAL | `Shared/MoodEntryFunctions.swift`:25 | Inner loop overwrites year grouping — zero test coverage |
|
||
| CRITICAL | `Shared/ShowBasedOnVoteLogics.swift`:94 | `fatalError` in production code — new `DayOptions` case crashes on launch |
|
||
| CRITICAL | `Shared/IAPManager.swift`:289 | `try?` grants premium on StoreKit error — zero IAPManager tests |
|
||
| CRITICAL | `Shared/Views/DayView/DayViewViewModel.swift`:1 | Primary view model directly accesses `DataController.shared` — completely untestable |
|
||
| BUG | `Shared/Date+Extensions.swift`:16 | Failable `init?(rawValue:)` never returns nil — bad input produces Jan 1, 2001 silently |
|
||
| BUG | `Shared/Date+Extensions.swift`:93 | `dateRange(monthInt:yearInt:)` hardcodes `-8 hours` — wrong for non-Pacific users |
|
||
| BUG | `Shared/Date+Extensions.swift`:45 | `toLocalTime()` double-applies timezone offset — wrong dates for non-UTC users |
|
||
| BUG | `Shared/IAPManager.swift`:40 | `bypassSubscription` is `false` in both `#if DEBUG` and `#else` branches — dead debug scaffold |
|
||
| BUG | `Shared/BGTask.swift`:42 | Error log typo `(error)` — scheduling failures always log literal text |
|
||
| BUG | `Shared/Views/YearView/YearViewModel.swift`:27 | `updateData()` doesn't populate `data` — transient blank year on cold launch |
|
||
| BUG | `Shared/Views/YearView/YearViewModel.swift`:48 | Double clear causes two empty-state flashes per refresh |
|
||
| BUG | `Shared/Views/MoodEntryFunctions.swift`:43 | Force-unwrap in `padMoodEntriesMonth` — crashes during DST spring-forward |
|
||
| BUG | `Shared/Onboarding/views/OnboardingSubscription.swift`:139 | Onboarding completion unconditional on dismiss |
|
||
| BUG | `Shared/Views/PersonalityPackPickerView.swift`:52 | `.alert` in `ForEach` — only last row fires |
|
||
| BUG | `Shared/Views/MonthView/MonthView.swift`:176 | `ForEach id: \.month` ID collision |
|
||
| BUG | `Shared/Views/MonthView/MonthView.swift`:661 | `cachedMetrics` only recalculated when empty — stale after mood edits |
|
||
| BUG | `Shared/Persisence/DataControllerDELETE.swift`:32 | `deleteRandomFromLast` in production code — accidental call irrecoverably deletes user data |
|
||
| BUG | `Shared/Views/SharingTemplates/LongestStreakTemplate.swift`:46 | Non-`@MainActor` init calls `@MainActor` function — Swift 6 violation |
|
||
| WARNING | `Shared/Analytics.swift`:24 | PostHog API key hardcoded — no test validates secure key loading |
|
||
| WARNING | `Shared/Models/UserDefaultsStore.swift`:217 | `cachedOnboardingData` race condition — no concurrency test |
|
||
| WARNING | `Shared/Views/EntryListView.swift`:2242 | Accelerometer never stopped — no lifecycle test |
|
||
| WARNING | `Shared/Views/CelebrationAnimations.swift`:109 | `asyncAfter` mood-save fires after dismissal — no test validates animation lifecycle |
|
||
| WARNING | `Shared/Services/ImageCache.swift`:24 | NotificationCenter observer leaked — no observer lifecycle test |
|
||
| WARNING | `Shared/Views/AccessibilityHelpers.swift`:33 | `value: UUID()` animation triggers every render — no test |
|
||
| WARNING | `Shared/Random.swift`:65 | `monthSymbols[fromMonthInt-1]` — no bounds check, called from 15+ views |
|
||
| WARNING | `Shared/Services/WatchConnectivityManager.swift`:117 | `pendingMoods` data race — no concurrency test |
|
||
| WARNING | `Shared/Views/SettingsView/LiveActivityPreviewView.swift`:325 | `ImageRenderer` on background thread — no threading test |
|
||
| WARNING | `Shared/Services/ExportService.swift`:688 | Force-unwrap in PDF generation critical path |
|
||
| WARNING | `Shared/IAPManager.swift`:466 | `listenForTransactions` task cancel in `deinit` — singleton never deinits, dead code |
|
||
| WARNING | `Shared/Views/InsightsView/InsightsViewModel.swift`:60 | Multiple concurrent `generateInsights()` — no concurrent state test |
|
||
| WARNING | `Shared/Views/InsightsView/InsightsViewModel.swift`:88 | `withTaskGroup` serialized — no performance baseline |
|
||
| WARNING | `Shared/Services/ReviewRequestManager.swift`:117 | Review request date recorded before dialog appears — no gating logic test |
|
||
| WARNING | `Shared/Services/HealthService.swift`:79 | `isAuthorized = true` unconditional — no auth state machine test |
|
||
| WARNING | `Shared/Models/MoodImagable.swift`:87 | All emoji image classes force-unwrap `textToImage()!` — crash under low memory |
|
||
| WARNING | `Shared/Onboarding/views/OnboardingWrapup.swift`:1 | `OnboardingWrapup` view not wired into `OnboardingMain` — dead code shipped in binary |
|
||
| WARNING | `Shared/Onboarding/views/OnboardingTitle.swift`:38 | All `OnboardingTitle` button actions commented out — view is a no-op |
|
||
| WARNING | `Tests macOS/Tests_macOS.swift`:27 | `testExample()` has no assertions — passes trivially |
|
||
| WARNING | `Tests macOS/Tests_macOS.swift`:34 | `testLaunchPerformance` has no baseline — no regression detection |
|
||
| WARNING | `Tests iOS/Tests_iOS.swift`:1 | XCTest only — zero Swift Testing adoption |
|
||
|
||
|
||
---
|
||
|
||
## Build Optimizer Auditor (38 findings)
|
||
|
||
| Severity | File | Finding |
|
||
|----------|------|---------|
|
||
| PERFORMANCE | `Shared/Views/EntryListView.swift`:1 | 2277-line file, 20-way `switch` in `body` — serializes compilation, no parallel build benefit |
|
||
| PERFORMANCE | `Shared/Views/FeelsSubscriptionStoreView.swift`:1 | 2394-line file with 12+ independent marketing structs — entire paywall recompiles on any change |
|
||
| PERFORMANCE | `Shared/Views/SettingsView/SettingsView.swift`:1 | 2118-line file with two near-duplicate views — duplicated type-checking cost |
|
||
| PERFORMANCE | `Shared/Views/LockScreenView.swift`:1 | 2035-line file, 30+ types, `AnyView` protocol — type erasure overhead + serialized compile |
|
||
| PERFORMANCE | `Shared/Views/DayView/DayView.swift`:1 | 934-line view with 16 `@ViewBuilder` section-header methods — large opaque result type graph |
|
||
| PERFORMANCE | `Shared/Views/MonthView/MonthView.swift`:1 | 860-line view with double `.onAppear` and `ForEach` ID collision adding reconciliation work |
|
||
| PERFORMANCE | `Shared/Views/CelebrationAnimations.swift`:1 | 850-line animation file — un-cancellable GCD callbacks, persistent `repeatForever` animations |
|
||
| PERFORMANCE | `Shared/Models/Theme.swift`:74 | `Themeable` protocol requires `AnyView` return types — forces type erasure across all 4 themes |
|
||
| PERFORMANCE | `Shared/Models/Shapes.swift`:17 | `AnyView` in `BGShape.view()` — defeats SwiftUI structural diffing for all entry cells |
|
||
| PERFORMANCE | `Shared/Views/SwitchableView.swift`:301 | `AnyView` for all switchable cases — full view replacement instead of identity diff |
|
||
| PERFORMANCE | `Shared/Views/Views/CustomizeView/CustomizeView.swift`:295 | Hidden `Text` re-render hack — fragile, may be optimized away by Apple |
|
||
| CRITICAL | `Shared/Persisence/DataControllerGET.swift`:74 | Infinite loop in `calculateStreak` |
|
||
| CRITICAL | `Shared/MoodStreakActivity.swift`:215 | Identical infinite loop in widget-path |
|
||
| CRITICAL | `Shared/AppShortcuts.swift`:136 | `while true` streak on `@MainActor` — thousands of fetches for long-streak users |
|
||
| CRITICAL | `FeelsWidget2/FeelsTimelineWidget.swift`:236 | Widget fetches from Unix epoch — entire history in 30MB widget process |
|
||
| BUG | `Shared/BGTask.swift`:42 | Error interpolation typo — scheduling failures invisible |
|
||
| BUG | `Shared/Views/CelebrationAnimations.swift`:109 | Un-cancellable `asyncAfter` mood-save |
|
||
| BUG | `Shared/SharedMoodIntent.swift`:93 | New `ModelContainer` per widget intent — SQLite contention |
|
||
| BUG | `Shared/FeelsApp.swift`:92 | `Task.detached { @MainActor in }` — Core Data on main thread at launch |
|
||
| BUG | `Shared/Views/EntryListView.swift`:2242 | `CMMotionManager` never stopped — continuous 60 Hz drain |
|
||
| BUG | `Shared/Models/BGView.swift`:52 | `randomMood` computed — all background cells re-randomize on every state change |
|
||
| PERFORMANCE | `FeelsWidget2/FeelsTimelineWidget.swift`:73 | `DateFormatter` computed property — new instance per widget render |
|
||
| PERFORMANCE | `FeelsWidget2/FeelsTimelineWidget.swift`:87 | `createViews` + `moodTintable()` called synchronously in widget view `init` |
|
||
| PERFORMANCE | `FeelsWidget2/FeelsVoteWidget.swift`:68 | `DateFormatter` + multiple UserDefaults JSON decodes per vote widget render |
|
||
| PERFORMANCE | `FeelsWidget2/WidgetProviders.swift`:127 | Reload policy `now+10s` vs entry date `now+15s` — unnecessary 5-second early reload |
|
||
| PERFORMANCE | `FeelsWidget2/WidgetModels.swift`:1 | `WatchTimelineView` is `class` — unexpected aliasing in WidgetKit timelines |
|
||
| PERFORMANCE | `Shared/Views/InsightsView/InsightsViewModel.swift`:88 | `withTaskGroup` child tasks all `@MainActor` — LLM calls serialized not parallel |
|
||
| PERFORMANCE | `Shared/Views/YearView/YearViewModel.swift`:36 | O(n log n) sort to find minimum date |
|
||
| PERFORMANCE | `Shared/Views/YearView/YearViewModel.swift`:48 | Double `removeAll()` — two empty-state re-renders |
|
||
| PERFORMANCE | `Shared/Models/MoodTintable.swift`:112 | `getCustomMoodTint()` called 10× per render — UserDefaults JSON decode on every access |
|
||
| PERFORMANCE | `Shared/Utilities/AccessibilityHelpers.swift`:33 | `value: UUID()` — animation triggers every render |
|
||
| PERFORMANCE | `Shared/Onboarding/views/OnboardingTime.swift`:13 | `DateFormatter` computed on DatePicker view — new instance every ~16ms during scroll |
|
||
| PERFORMANCE | `Shared/MoodLogger.swift`:134 | `ISO8601DateFormatter()` per call to `markSideEffectsApplied` |
|
||
| PERFORMANCE | `Shared/Random.swift`:28 | `groupDefaults` computed static var — `UserDefaults(suiteName:)` on every hot-path access |
|
||
| PERFORMANCE | `Shared/Random.swift`:73 | `NumberFormatter` created inside `dayFormat()` on every call |
|
||
| PERFORMANCE | `Shared/Views/DayView/DayView.swift`:17 | Dead `@AppStorage deleteEnabled` — unnecessary UserDefaults subscription on root list view |
|
||
| WARNING | `Shared/Views/MonthView/MonthView.swift`:410 | `@ObservedObject demoManager` singleton in `Equatable` struct — excluded from equality check |
|
||
| WARNING | `Shared/Services/HealthService.swift`:440 | 365 concurrent `group.addTask` for HealthKit — floods cooperative thread pool |
|
||
|
||
|
||
---
|
||
|
||
## Codable/Networking Auditor (31 findings)
|
||
|
||
| Severity | File | Finding |
|
||
|----------|------|---------|
|
||
| CRITICAL | `Shared/Color+Codable.swift`:33 | `encode(to:)` throws at runtime when `cgColor` is nil — semantic colors crash encode |
|
||
| CRITICAL | `Shared/IAPManager.swift`:289 | `try?` on `subscription.status` — StoreKit error silently grants premium access |
|
||
| CRITICAL | `Shared/Persisence/DataControllerGET.swift`:74 | Infinite loop in `calculateStreak` when `date(byAdding:)` returns nil |
|
||
| CRITICAL | `Shared/MoodStreakActivity.swift`:215 | Same infinite loop in `LiveActivityScheduler.calculateStreak` |
|
||
| CRITICAL | `Shared/Persisence/ExtensionDataProvider.swift`:199 | Non-atomic delete-save-insert — widget mood log can permanently delete entry |
|
||
| BUG | `Shared/Color+Codable.swift`:84 | `Scanner.scanHexInt64` return value discarded — invalid hex silently produces black |
|
||
| BUG | `Shared/Color+Codable.swift`:86 | Non-6/8 char hex silently produces black — no validation |
|
||
| BUG | `Shared/Color+Codable.swift`:137 | `RawRepresentable` for Color drops alpha channel — `@AppStorage` loses transparency |
|
||
| BUG | `Shared/Onboarding/OnboardingData.swift`:52 | `rawValue` returns `"[]"` on encode failure — next decode produces nil, resets onboarding |
|
||
| BUG | `Shared/LocalNotification.swift`:21 | `testIfEnabled` completion never called when user denies without error — permanent hang |
|
||
| BUG | `Shared/Date+Extensions.swift`:16 | Failable `init?(rawValue:)` never returns nil — bad string produces Jan 1, 2001 |
|
||
| BUG | `Shared/SharedMoodIntent.swift`:109 | `try?` delete-save + non-atomic insert — permanent data loss path |
|
||
| BUG | `Shared/BGTask.swift`:42 | Error interpolation typo — `(error)` literal always printed |
|
||
| BUG | `Shared/Services/WatchConnectivityManager.swift`:117 | `pendingMoods` data race — concurrent read/write without synchronization |
|
||
| BUG | `Shared/Services/ExportService.swift`:688 | Force-unwrap `calendar.date(byAdding:)` in PDF generation |
|
||
| BUG | `Shared/Models/MoodTintable.swift`:89 | `Color.init(from:)` calls `init(hex:)` which never throws — invalid hex decoded as black |
|
||
| WARNING | `Shared/Onboarding/OnboardingData.swift`:41 | `try?` in `init?(rawValue:)` silently resets reminder on schema change |
|
||
| WARNING | `Shared/Models/UserDefaultsStore.swift`:227 | `try?` decode `OnboardingData` silently returns defaults on corruption |
|
||
| WARNING | `Shared/Models/UserDefaultsStore.swift`:315 | `try?` decode `[CustomWidgetModel]` on failure calls `removeObject` — wipes all user widgets |
|
||
| WARNING | `Shared/Models/UserDefaultsStore.swift`:324 | `try?` encode widgets — unperisted widgets reappear on next launch |
|
||
| WARNING | `Shared/Models/UserDefaultsStore.swift`:330 | Double `try?` decode in fallback path |
|
||
| WARNING | `Shared/Models/UserDefaultsStore.swift`:309 | `synchronize()` deprecated no-op |
|
||
| WARNING | `Shared/Models/UserDefaultsStore.swift`:251 | `print` only error handling for `JSONEncoder` failures in `saveOnboarding` |
|
||
| WARNING | `Shared/Models/UserDefaultsStore.swift`:365 | `print` only error handling in `saveCustomWidget` |
|
||
| WARNING | `Shared/Models/UserDefaultsStore.swift`:395 | `print` only error handling in `deleteCustomWidget` — deleted widget reappears after crash |
|
||
| WARNING | `Shared/MoodLogger.swift`:73 | `try?` swallows HealthKit save error — sync silently fails without log entry |
|
||
| WARNING | `Shared/MoodStreakActivity.swift`:139 | `try?` in `getUserRatingTime` — nil blocks all Live Activity scheduling silently |
|
||
| WARNING | `Shared/LocalNotification.swift`:35 | `try?` in `rescheduleNotifiations` — decode fail silently stops all future notifications |
|
||
| WARNING | `Shared/Persisence/DataControllerGET.swift`:24 | `try?` on `modelContext.fetch` — database error indistinguishable from "no entry" |
|
||
| WARNING | `Shared/Persisence/DataControllerADD.swift`:43 | `try?` on fetch in `fillInMissingDates` — silent failure leaves history gaps permanently |
|
||
| WARNING | `Shared/Analytics.swift`:552 | Force-unwrap after nil check — antipattern, crash risk on future refactor |
|
||
| WARNING | `Shared/MoodLogger.swift`:134 | `ISO8601DateFormatter()` allocated per call — expensive on hot path |
|
||
| WARNING | `Shared/Services/ExportService.swift`:49 | `ExportService` formatters not thread-safe — concurrent export causes garbled dates |
|
||
|
||
|
||
---
|
||
|
||
## Cross-Cutting Deep Audit (20 findings)
|
||
|
||
| Severity | File | Finding |
|
||
|----------|------|---------|
|
||
| CRITICAL | `Shared/Persisence/DataControllerGET.swift`:74 | **Infinite loop** — `while true` + `?? checkDate` never advances on nil Calendar result; confirmed in MoodStreakActivity.swift:215 and AppShortcuts.swift:128–140 |
|
||
| CRITICAL | `Shared/Persisence/DataControllerADD.swift`:13 | **Data loss** — non-atomic delete-save-insert; old entry deleted, new entry not persisted if second save fails; duplicated in ExtensionDataProvider.swift:199 and SharedMoodIntent.swift:109 |
|
||
| CRITICAL | `Shared/SharedMoodIntent.swift`:93 | **Store corruption** — new `ModelContainer` per widget intent against live SQLite file; `ExtensionDataProvider` correctly caches; intent does not |
|
||
| CRITICAL | `Shared/BGTask.swift`:42 | **Invisible failures** — `(error)` typo, all scheduling failures print literal text; `@MainActor` annotation runs Core Data on main thread; `setTaskCompleted` fires before async work completes |
|
||
| CRITICAL | `Shared/IAPManager.swift`:271 | **Revenue leak** — `try?` on `subscription.status` falls through to unconditional `state = .subscribed` on any StoreKit error |
|
||
| CRITICAL | `Shared/MoodStreakActivity.swift`:38 | **Race condition** — `startStreakActivity` uses an unstructured Task; rapid concurrent calls start multiple Live Activities; orphaned activities persist on Lock Screen |
|
||
| CRITICAL | `Shared/Persisence/DataControllerADD.swift`:46 | **Data integrity** — future-dated entry causes `fillInMissingDates` to insert entries for every day from now to that future date |
|
||
| CRITICAL | `Shared/MoodLogger.swift`:108 | **Logic error** — `processPendingSideEffects` uses `getData(includedDays: [])` = "all days", bypassing user's day-filter setting; streak counter differs from filtered day view |
|
||
| CRITICAL | `Shared/Views/SettingsView/SettingsTabView.swift`:61 | **Guaranteed crash** — `CustomizeContentView` missing `iapManager`/`authManager` environment objects |
|
||
| CRITICAL | `Shared/Onboarding/views/OnboardingSubscription.swift`:139 | **Onboarding loop** — completion closure fires on every sheet dismiss including cancel; `onboardingCompleted` event fires twice when user visits paywall then uses skip path |
|
||
| BUG | `Shared/Models/UserDefaultsStore.swift`:217 | **Data race** — static `cachedOnboardingData` accessed from main actor and BGTask without synchronization; wrong dates propagate to CloudKit |
|
||
| BUG | `Shared/Persisence/DataController.swift`:51 | **Memory leak + stale callbacks** — `editedDataClosure` grows forever, stale closures from deallocated ViewModels called on every save; no removal mechanism |
|
||
| BUG | `Shared/Views/CelebrationAnimations.swift`:109 | **Ghost entries** — `DispatchQueue.asyncAfter` mood-save fires after view dismissal; duplicate entries possible on rapid navigation |
|
||
| BUG | `Shared/Persisence/DataControllerGET.swift`:18 | **Off-by-one** — `entry.forDate <= endDate` where `endDate` is midnight of next day; 8 identical predicates across codebase |
|
||
| BUG | `Shared/Services/WatchConnectivityManager.swift`:117 | **Data race** — `pendingMoods` read/written from WCSession delegate background thread and main thread without any synchronization |
|
||
| BUG | `Shared/Models/Color+Codable.swift`:33 | **Silent encode crash** — `encode(to:)` throws when `cgColor` is nil for semantic colors; custom theme silently lost |
|
||
| BUG | `Shared/Date+Extensions.swift`:16 | **Silent bad date** — failable `init?(rawValue:)` never returns nil; bad string produces Jan 1, 2001, breaking trial expiration for first-launch users |
|
||
| BUG | `Shared/Models/MoodEntryFunctions.swift`:25 | **Data display corruption** — `newGrouped[year] = newMonth` inside inner month loop overwrites all months; confirmed by source read; all months but last invisible for multi-year users |
|
||
| BUG | `Shared/Persisence/DataController.swift`:55 | **Silent failure** — `saveAndRunDataListeners()` runs listeners even when `save()` fails; UI shows unsaved state as if it were persisted |
|
||
| BUG | `CLAUDE.md`:24 | **Doc mismatch** — documentation says `Shared/Persistence/` (correct spelling) but actual directory is `Shared/Persisence/` (typo) and files named `DataController*.swift` not `Persistence*.swift` |
|
||
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
### Finding Counts by Auditor
|
||
|
||
| Auditor | Findings |
|
||
|---------|----------|
|
||
| Memory Auditor | 15 |
|
||
| IAP Auditor | 16 |
|
||
| Concurrency Auditor | 33 |
|
||
| SwiftUI Performance Auditor | 33 |
|
||
| Security Auditor | 33 |
|
||
| Accessibility Auditor | 51 |
|
||
| Energy/Battery Auditor | 23 |
|
||
| Storage Auditor | 28 |
|
||
| iCloud/CloudKit Auditor | 22 |
|
||
| SwiftUI Architecture Auditor | 49 |
|
||
| Swift Performance Auditor | 31 |
|
||
| Modernization Auditor | 43 |
|
||
| Navigation Auditor | 27 |
|
||
| Testing Auditor | 47 |
|
||
| Build Optimizer Auditor | 38 |
|
||
| Codable/Networking Auditor | 33 |
|
||
| Cross-Cutting Deep Audit | 20 |
|
||
| **TOTAL** | **542** |
|
||
|
||
### Finding Counts by Severity
|
||
|
||
| Severity | Count |
|
||
|----------|-------|
|
||
| CRITICAL | ~85 |
|
||
| BUG | ~185 |
|
||
| PERFORMANCE | ~145 |
|
||
| WARNING | ~127 |
|
||
|
||
### Files with Most Findings
|
||
|
||
| File | Approx Findings | Primary Issues |
|
||
|------|-----------------|----------------|
|
||
| `Shared/Persisence/DataControllerGET.swift` | 8 | Infinite loop, off-by-one predicate, epoch fetch |
|
||
| `Shared/Views/EntryListView.swift` | 12 | Accelerometer leak, accessibility, performance |
|
||
| `Shared/Views/SettingsView/SettingsTabView.swift` | 7 | Missing env objects (crash), deprecated APIs |
|
||
| `Shared/Persisence/DataControllerADD.swift` | 8 | Non-atomic delete-insert, data loss, future-dated entries |
|
||
| `Shared/IAPManager.swift` | 9 | try? grants premium, trial clock, revocation |
|
||
| `Shared/Analytics.swift` | 6 | Hardcoded API key, race conditions, concurrency |
|
||
| `Shared/BGTask.swift` | 6 | @MainActor annotation, typo, timing bugs |
|
||
| `Shared/Views/MonthView/MonthView.swift` | 10 | ForEach ID collision, singletons in @StateObject |
|
||
| `Shared/MoodStreakActivity.swift` | 7 | Infinite loop, timer safety, race condition |
|
||
| `Shared/Models/MoodEntryFunctions.swift` | 3 | Inner loop overwrites year — critical data display bug |
|
||
|
||
---
|
||
|
||
## TOP 10 PRIORITIES
|
||
|
||
These are the highest-impact issues to fix first, ordered by severity and blast radius:
|
||
|
||
### 🔴 P1 — Fix Immediately (Data Loss / Security / Crash)
|
||
|
||
**1. Infinite Loop in Streak Calculation** — `DataControllerGET.swift:74`, `MoodStreakActivity.swift:215`, `AppShortcuts.swift:128–140`
|
||
- Three separate `while true` + `?? checkDate` loops that never terminate if `Calendar.date(byAdding:)` returns nil
|
||
- Fix: Replace `while true` with a bounded loop (`for _ in 0..<3650`) or use the batch-fetch + in-memory iteration pattern already implemented in `getCurrentStreak(includedDays:)` (which correctly iterates over pre-fetched array)
|
||
|
||
**2. Non-Atomic Delete-Save-Insert Pattern** — `DataControllerADD.swift:13`, `ExtensionDataProvider.swift:204`, `SharedMoodIntent.swift:109`
|
||
- Delete old entry → `try? save()` → insert new entry → save. If second save fails, entry permanently gone
|
||
- Fix: Use SwiftData's rollback capability — only call `save()` once after both the delete and insert; wrap in a `do/catch` and rollback on failure
|
||
|
||
**3. Hardcoded PostHog API Key** — `Analytics.swift:24`
|
||
- Production API key committed to git history permanently — rotate the key in PostHog immediately
|
||
- Fix: Rotate key in PostHog dashboard NOW; load from xcconfig/Info.plist not committed to repo
|
||
|
||
**4. `CustomizeContentView` Missing Environment Objects** — `SettingsTabView.swift:61`
|
||
- Any user navigating to the Customize tab in Settings crashes the app with a fatal missing EnvironmentObject error
|
||
- Fix: Add `.environmentObject(iapManager).environmentObject(authManager)` to `CustomizeContentView()` at line 62
|
||
|
||
**5. `try?` on StoreKit Status Grants Premium on Error** — `IAPManager.swift:289`
|
||
- Any StoreKit network error during subscription status check causes the fallback at line 333 to grant `.subscribed` state
|
||
- Fix: Replace `try?` with `do { let statuses = try await ... } catch { /* log, set state to unknown */ return false }`
|
||
|
||
### 🟠 P2 — Fix Before Next Release (Significant Impact)
|
||
|
||
**6. Month Calendar Shows Only Last Month Per Year** — `MoodEntryFunctions.swift:25`
|
||
- `newGrouped[year] = newMonth` is inside the inner month loop — every iteration overwrites the year entry, leaving only the last month
|
||
- Fix: Move `newGrouped[year] = newMonth` to after the inner `for key in monthKeys` loop closes
|
||
|
||
**7. ForEach ID Collision Across Years in MonthView** — `MonthView.swift:176`
|
||
- `id: \.element.month` (integer 1–12) collides across years — January 2023 and January 2024 share ID 1, one is silently dropped
|
||
- Fix: Use `id: \.element` (the full `WatchTimelineView` object) or a composite ID like `"\(year)-\(month)"`
|
||
|
||
**8. BGTask Scheduling Failure Invisible + Wrong Thread** — `BGTask.swift:14, 42`
|
||
- `(error)` typo makes all failures invisible; `@MainActor` annotation runs expensive Core Data fill on main thread
|
||
- Fix: Fix typo (`\(error)`); remove `@MainActor`, dispatch work to background actor
|
||
|
||
**9. `ModelContainer` Created Per Widget Intent** — `SharedMoodIntent.swift:93`
|
||
- Every widget tap creates a new SQLite connection against the live store — concurrent writers can corrupt WAL state
|
||
- Fix: Add a `_container: ModelContainer?` lazy cache pattern (already present in `ExtensionDataProvider`)
|
||
|
||
**10. Missing `PrivacyInfo.xcprivacy` Manifest**
|
||
- App Store requires `PrivacyInfo.xcprivacy` for all apps using `UserDefaults`, `NSUserDefaults`, and third-party SDKs (PostHog) since May 2024
|
||
- Fix: Create `PrivacyInfo.xcprivacy` declaring `NSPrivacyAccessedAPICategoryUserDefaults` with reason code `CA92.1` (or applicable), and PostHog's required API types
|
||
|