124 KiB
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 ininit(). The returned opaque observer token is discarded entirely with no variable. The block-based API requires the token to be stored and passed toremoveObserver(_:)indeinit. - Impact: Each
ImageCacheinitialization registers an observer that is never deregistered. In extension contexts whereImageCachemight be initialized more than once, each initialization accumulates another dangling registration that firesclearCache()on every memory warning.
Shared/Persisence/DataController.swift:51-53 | CRITICAL | editedDataClosure array grows forever — no removal mechanism
- What:
addNewDataListener(closure:)appends closures toeditedDataClosurewith no paired remove function. Every call site permanently stores its closure inside the singletonDataController.shared. If those closures captureself(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
addNewDataListenerleaks 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.bodycallsmotionManager.startIfNeeded()in.onAppear(starts CMMotionManager at 30 Hz), but there is no.onDisappearmodifier callingmotionManager.stop(). Thestop()function exists but is never invoked from any view. - Impact: Accelerometer runs at 30 Hz continuously for the entire app session once any
MotionCardViewappears. 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).asyncat line 325 callsImageRenderer(content:).uiImageon a background thread.ImageRendereris@MainActorand 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:
WatchConnectivityManagerhas no actor isolation.pendingMoodsis appended insendMoodToPhone(called from any context) and read+cleared inactivationDidCompleteWith(called on a WCSession background queue). No lock, serial queue, or actor isolation protects concurrent access. - Impact: Data race on
pendingMoodscan 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 trueloop advancescheckDateviacalendar.date(byAdding: .day, value: -1, to: checkDate) ?? checkDate. Ifdate(byAdding:)returnsnil,checkDatedoes 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 viaasyncAfter. 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()andrestartAnimation()scheduleasyncAfterwith 3-second delay to callbeginAnimation().restartAnimation()invalidates the Timer but cannot cancel the GCD block. Rapid calls queue multiplebeginAnimation()invocations. - Impact: Multiple
Timerinstances run simultaneously at 60 Hz, each mutatinganimationProgress, 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 causinganimationProgressto jump discontinuously. - Impact: Animation stutters. Up to 60
Taskobjects per second simultaneously alive, each holding a strong reference toself.
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 aTask.detachedtagged@MainActor. The@MainActorannotation forces entire body back onto the main thread —detachedprovides 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.detachedclosures forscheduleBasedOnCurrentTime()andprocessPendingSideEffects()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 withscheduleStartwhich 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, butIAPManageris a singleton anddeinitis never called in production. The[weak self]anddeinitcancel are dead code creating a misleading code contract. - Impact: If
IAPManageris ever instantiated non-singleton (e.g., in tests), the task continues iteratingTransaction.updatesafter 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()createsTask{}without storing the handle. Multiple.onAppearor pull-to-refresh calls can spawn concurrent tasks writing to the same@Publishedarrays. - Impact: Flickering loading states; potential for two in-flight
LanguageModelSessioninstances consuming memory.
SOURCE: IAP Auditor (16 findings)
Shared/IAPManager.swift:274 | CRITICAL | Transaction.currentEntitlements never calls transaction.finish()
- What: The loop iterating
currentEntitlementsprocesses transactions but never callsawait transaction.finish(). StoreKit 2 requiresfinish()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.statusdiscards StoreKit errors. If the call throws (network offline, StoreKit unavailable),statusesisniland execution falls through to line 333 which grants.subscribedas 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(...)wheneverproduct.subscriptionis nil orsubscription.statusfails. 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,checkForActiveSubscriptionreturns false, the terminal guard is skipped (state was reset), and the cache fallback (line 207) grants.subscribedagain. - 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
.subscribedfrom 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 spawningTask { @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:)callscompletionClosure(onboardingData)inonDismisswhether the user subscribed or cancelled the sheet. - Impact: Onboarding completes even when user dismisses paywall without subscribing. Also fires
onboardingCompletedanalytics 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 callstransaction.finish()on allTransaction.updates. But the initial check incheckForActiveSubscriptionnever finishes transactions fromcurrentEntitlements. - Impact: Transactions delivered at launch via
currentEntitlementswill be re-delivered viaTransaction.updates, causing double-processing.
Shared/Views/FeelsSubscriptionStoreView.swift:54-55 | WARNING | .pending purchase state tracked as failure in analytics
- What:
case .success(.pending)callstrackPurchaseFailed(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
hasActiveSubscriptionistruebut no expiration date is stored, grantsstate = .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 DEBUGvariant 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 incatchblock only callsprint(...). 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.expiredor.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 useString(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() }ininit()has no handle stored.updateListenerTaskis separately cancelled indeinit, 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:
pendingMoodsis a plainvararray on a class with no actor annotation. It is written insendMoodToPhone(called from watchOS app context) and both read and written inactivationDidCompleteWith(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
sendMessageerror handler closure at line 93 callssession.transferUserInfo(message)directly. WCSession error callbacks run on an arbitrary background thread.WatchConnectivityManagerhas 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
detachedkeyword 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:
runFillInMissingDatesTaskis marked@MainActorand callsMoodLogger.shared.processPendingSideEffects()at line 25 withoutawait. Calling it withoutawaitmeans it begins execution but the caller does not wait for it to finish. - Impact:
task.setTaskCompleted(success: true)fires beforeprocessPendingSideEffects()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:
runFillInMissingDatesTaskis 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 previousdate(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.submitfailures 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@MainActorcontexts 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 fromtextToImage()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 vardictionaries are read/written fromweekdayName(fromDate:)anddayFormat(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()callsanimationTimer?.invalidate()thenDispatchQueue.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
animationProgressstate cause undefined animation behavior and CPU waste.
Shared/MoodStreakActivity.swift:215-223 | CRITICAL | Infinite loop when Calendar.date(byAdding:) returns nil
- What:
while trueloop with?? checkDatefallback — ifdate(byAdding: .day, value: -1, to: checkDate)returns nil,checkDatenever changes and the loop never terminates. - Impact: Infinite loop on
@MainActorpermanently freezes the UI.
Shared/Persisence/DataControllerGET.swift:74-82 | CRITICAL | Infinite loop when date(byAdding:) returns nil in calculateStreak
- What: Same
while truewith?? checkDatefallback incalculateStreak(from:). On DST edge case,checkDatedoesn'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 awhile trueloop 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@MainActorinitializer is called off-actor. - Impact: SwiftData
ModelContainerandModelContextcreated 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()launchesTask { await generateAllInsights() }with no guard. Multiple concurrent tasks independently overwrite@Publishedarrays. - Impact: Race between two tasks writing the same
@Publishedproperty. 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:)callsAnalyticsManager.shared.trackScreen(...)inside.onAppear. Under Swift 6, calling a@MainActorfunction 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:)andloadThumbnail(id:)callData(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 }.ImageRendereris@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 freshModelContaineron every call. MultipleModelContainerwriters 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.initis not@MainActor-isolated but callsself.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
@Statevars at init time.
Shared/Views/Photos/PhotoPickerView.swift:151 | BUG | @State mutation from non-@MainActor async context
- What:
loadImage(from:)is declaredprivate func ... asyncwith no@MainActorannotation. Inside it,isProcessing = truemutates a@Stateproperty off the main actor. - Impact: Swift 6 data race: writing
@Stateoff 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 insideBindingset closures mutate@Publishedproperties of@EnvironmentObjectObservableObjects without explicit@MainActorannotation. - Impact: Under Swift 6, setting
@Publishedproperties ofObservableObjecton 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.detachedclosures forscheduleBasedOnCurrentTime()andprocessPendingSideEffects()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.scheduledTimercallback directly sets@Stateproperties. While timers on the main run loop fire on the main thread, there is no enforcement thatstartAnimation()is only ever called on the main thread. - Impact: If called from a non-main context, mutates
@Stateoff 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()returnsTask.detached { [weak self] in ... }. On a singleton,[weak self]never becomes nil. Thedeinit { updateListenerTask?.cancel() }is also dead code. Future code added directly in the loop body withoutawait 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
@preconcurrencyto silence Swift concurrency warnings. Under Swift 6,@preconcurrencysuppresses 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
selfis 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
includedDaysarray 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 <= endDatewhereendDateis 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: UIImageas a plain computed property callingsomeView.asImage(size:). IfasImage()usesImageRenderer(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 inbody. TheDatePickerbound to$onboardingData.dateupdates 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: DateFormatteris 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: DateFormatterandprivate var dateFormatter: DateFormatterare computed properties constructing newDateFormatterinstances. Repeated inSmallWidgetView,MediumWidgetView, andLargeWidgetView. - Impact: Widget rendering is memory-constrained. Multiple
DateFormatterobjects 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: Stringcreates a freshDateFormatterinline per call. ThirdDateFormatterallocation perMediumWidgetViewrender. - 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
dayFormatteranddateFormatterinside the function body. Accessed directly frombody. - Impact: Two
DateFormatterallocations 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 sortedGroupedDatacalls.sorted { $0.key > $1.key }twice and constructs a new array on every body evaluation. UnlikeMonthViewwhich caches in@State,DayViewre-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
MonthCardholds an@ObservedObjectreference to the same singleton. WhendemoManager.animationProgresspublishes at 60 fps during demo mode, allMonthCardviews receiveobjectWillChange, 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.mapon 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,entrieschanges butcachedMetricsis non-empty so never recalculates.Equatableconformance checks only count, not content. - Impact: After editing a mood entry,
MonthCardshows old mood distribution stats. Share sheet also usescachedMetrics, 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.cachedMetricscalculated only when empty. If entry moods change without changing count,YearCardnever 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:
GeometryReaderembedded in scroll content.backgroundto measure scroll offset and emit via preference key. FiresonPreferenceChangeon every single scroll position change. Same pattern inMonthView. - 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?.forDatesorts 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 separateremoveAll()calls each publishobjectWillChangebefore 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.bodyapplies.fill(mood.color.opacity(Double.random(in: 0.5...1.0)))insideForEach. 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:
classicStylecallsRandom.weekdayNameandRandom.dayFormatdirectly, bypassingDateFormattingCache.shared. Same bypass inmicroStyleandMotionCardView. - Impact:
classicStyleis 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 inwithAnimation(.interactiveSpring(...)), re-enqueuing an animation transaction at 30 Hz. - Impact: 30 animation transactions per second on the main thread for all visible
MotionCardViewcells 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
repeatForeveranimations in.onAppear..onDisappearresets 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:
ForecastLockBackgroundusesCGFloat.random(in: 40...80)inline inside the view body for cloud element sizes and blur values. Not captured in@StateoronAppear. - 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
initcallsTimeLineCreator.createViews(daysBack:)andUserDefaultsStore.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:
headerViewcallsShowBasedOnVoteLogics.isMissingCurrentVote(onboardingData: UserDefaultsStore.getOnboarding())on every body pass. Decode path reachable on cache miss. - Impact: Every body re-render of
DayViewcalls intoUserDefaultsStore.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:
.maskmodifier uses(condition) ? AnyView(LinearGradient(...)) : AnyView(Color.black). SwiftUI cannot track structural identity throughAnyView. Same pattern inYearViewat lines 184-195. - Impact: Entire
ScrollViewcontent 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:
WrappedSharablestructs useAnyViewfor.previewand.destination.ForEachoverAnyViewitems 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:)andCustomMoodTint.secondary(forMood:)each callUserDefaultsStore.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/60second; each fire wrapsupdateProgress()inTask { @MainActor in ... }. Under load, Tasks queue up and multipleupdateProgress()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 moodTintandprivate var moodImagesare computed properties triggering UserDefaults reads on every access. Called multiple times withinbody. - 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:
customWidgetreads UserDefaults each access; called twice inbody, 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:
MoodLoggercreates a newISO8601DateFormatter()at lines 134 and 141 inside date key functions. Called every time the user logs a mood. - Impact:
ISO8601DateFormatterhas similar initialization cost toDateFormatter. Should bestatic letproperties.
Shared/Views/ExportView.swift:177-178 | PERFORMANCE | DateFormatter created inside dateRangeText computed property on every call
- What:
private var dateRangeText: StringcreatesDateFormatterinline. Called frombodyduring export progress updates. - Impact: Each
ExportViewbody evaluation allocates and discards aDateFormatter.
Shared/Views/AddMoodHeaderView.swift:343-344 | WARNING | repeatForever animation started in .onAppear — .onDisappear resets value but cannot cancel in-flight repeat
- What:
OrbitVotingView.onAppearstartsrepeatForeveranimation oncenterPulse..onDisappearsetscenterPulse = 1.0nominally stopping it. Rapid navigation can cause animation to restart from intermediate state. Same pattern inNeonVotingViewlines 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
MoodEntryModelobjects 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
.onAppearmodifiers: one fires analytics, one populatescachedSortedData. SwiftUI ordering of multiple.onAppearhandlers 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, andGOOGLE_APP_IDcommitted 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, andconfig.captureNetworkTelemetry = trueset 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 onisUnlockedare 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 setsisLockEnabled = falseandisUnlocked = truewith 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, callstry? modelContext.save(), then inserts and callssaveAndRunDataListeners(). 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.groupDefaultsforce-unwrapsUserDefaults(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 insideSettingsTabViewwithout.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_enabledandhealthkit_enabledare 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 writesfalseto UserDefaults, then callsPostHogSDK.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()ifcanUseDevicePasscodeis 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:
cachedSubscriptionExpirationis a plainDatein 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
hasActiveistrueandcachedExpirationisnil, grantsstate = .subscribed(expirationDate: nil, willAutoRenew: false)unconditionally. - Impact:
nilexpiration 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, andstatic var textToImageCacheare 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. Passing0produces[-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
pendingMoodsarray, 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 = trueandisEnabled = trueare 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 ofAppLogger.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
falseto 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 DEBUGguard. - Impact: Any code path that accidentally calls
deleteRandomFromLastirreversibly 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)")!whereurl.pathis 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:
onDismisshandler callscompletionClosure(onboardingData)regardless of whether user subscribed.onboardingCompletedanalytics 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
.xcprivacyfile 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:
sessionReplayEnabledreturnstruewhen 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:)andloadThumbnail(id:)callData(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+?? checkDateloops that never terminate ifCalendar.date(byAdding:)returns nil - Fix: Replace
while truewith a bounded loop (for _ in 0..<3650) or use the batch-fetch + in-memory iteration pattern already implemented ingetCurrentStreak(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 ado/catchand 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)toCustomizeContentView()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
.subscribedstate - Fix: Replace
try?withdo { 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] = newMonthis inside the inner month loop — every iteration overwrites the year entry, leaving only the last month- Fix: Move
newGrouped[year] = newMonthto after the innerfor key in monthKeysloop 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 fullWatchTimelineViewobject) or a composite ID like"\(year)-\(month)"
8. BGTask Scheduling Failure Invisible + Wrong Thread — BGTask.swift:14, 42
(error)typo makes all failures invisible;@MainActorannotation 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 inExtensionDataProvider)
10. Missing PrivacyInfo.xcprivacy Manifest
- App Store requires
PrivacyInfo.xcprivacyfor all apps usingUserDefaults,NSUserDefaults, and third-party SDKs (PostHog) since May 2024 - Fix: Create
PrivacyInfo.xcprivacydeclaringNSPrivacyAccessedAPICategoryUserDefaultswith reason codeCA92.1(or applicable), and PostHog's required API types