Files
Reflect/Shared/ReflectApp.swift
Trey T 1f040ab676 v1.1 polish: accessibility, error logging, localization, and code quality sweep
- Wrap 30+ production print() statements in #if DEBUG guards across 18 files
- Add VoiceOver labels, hints, and traits to Watch app, Live Activities, widgets
- Add .accessibilityAddTraits(.isButton) to 15+ onTapGesture views
- Add text alternatives for color-only indicators (progress dots, mood circles)
- Localize raw string literals in NoteEditorView, EntryDetailView, widgets
- Replace 25+ silent try? with do/catch + AppLogger error logging
- Replace hardcoded font sizes with semantic Dynamic Type fonts
- Fix FIXME in IconPickerView (log icon change errors)
- Extract magic animation delays to named constants across 8 files
- Add widget empty state "Log your first mood!" messaging
- Hide decorative images from VoiceOver, add labels to ColorPickers
- Remove stale TODO in Color+Codable (alpha change deferred for migration)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:09:14 -05:00

165 lines
6.7 KiB
Swift

//
// ReflectApp.swift
// Shared
//
// Created by Trey Tartt on 1/5/22.
//
import SwiftUI
import SwiftData
import BackgroundTasks
import WidgetKit
@main
struct ReflectApp: App {
private enum AnimationConstants {
static let deepLinkHandlingDelay: TimeInterval = 0.3
}
@Environment(\.scenePhase) private var scenePhase
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
let dataController = DataController.shared
@StateObject var iapManager = IAPManager.shared
@StateObject var authManager = BiometricAuthManager()
@StateObject var healthKitManager = HealthKitManager.shared
@AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults) private var firstLaunchDate = Date()
@State private var showSubscriptionFromWidget = false
@State private var showStorageFallbackAlert = SharedModelContainer.isUsingInMemoryFallback
init() {
// Configure UI test mode before anything else
if UITestMode.isUITesting {
UITestMode.configureIfNeeded()
}
AnalyticsManager.shared.configure()
BGTaskScheduler.shared.cancelAllTaskRequests()
BGTaskScheduler.shared.register(forTaskWithIdentifier: BGTask.updateDBMissingID, using: nil) { (task) in
guard let processingTask = task as? BGProcessingTask else { return }
BGTask.runFillInMissingDatesTask(task: processingTask)
}
BGTaskScheduler.shared.register(forTaskWithIdentifier: BGTask.weatherRetryID, using: nil) { task in
guard let processingTask = task as? BGProcessingTask else { return }
BGTask.runWeatherRetryTask(task: processingTask)
}
UNUserNotificationCenter.current().setBadgeCount(0)
// Reset tips session on app launch
ReflectTipsManager.shared.resetSession()
// Initialize Live Activity scheduler
LiveActivityScheduler.shared.scheduleBasedOnCurrentTime()
// Initialize Watch Connectivity for cross-device widget updates
_ = WatchConnectivityManager.shared
}
var body: some Scene {
WindowGroup {
ZStack {
MainTabView(dayView: DayView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: false)),
monthView: MonthView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: true)),
yearView: YearView(viewModel: YearViewModel()),
insightsView: InsightsView())
.modelContainer(dataController.container)
.environmentObject(iapManager)
.environmentObject(authManager)
.environmentObject(healthKitManager)
.sheet(isPresented: $showSubscriptionFromWidget) {
ReflectSubscriptionStoreView(source: "widget_deeplink")
.environmentObject(iapManager)
}
.onOpenURL { url in
handleDeepLink(url)
}
.alert("Data Storage Unavailable",
isPresented: $showStorageFallbackAlert) {
Button("OK", role: .cancel) { }
.accessibilityIdentifier(AccessibilityID.AppAlert.storageUnavailableOK)
} message: {
Text("Your mood data cannot be saved permanently. Please restart the app. If the problem persists, reinstall the app.")
}
.onAppear {
if SharedModelContainer.isUsingInMemoryFallback {
AnalyticsManager.shared.track(.storageFallbackActivated)
}
if let url = AppDelegate.pendingDeepLinkURL {
AppDelegate.pendingDeepLinkURL = nil
DispatchQueue.main.asyncAfter(deadline: .now() + AnimationConstants.deepLinkHandlingDelay) {
handleDeepLink(url)
}
}
}
// Lock screen overlay
if authManager.isLockEnabled && !authManager.isUnlocked {
LockScreenView(authManager: authManager)
.transition(.opacity)
}
}
}.onChange(of: scenePhase) { _, newPhase in
if newPhase == .background {
BGTask.scheduleBackgroundProcessing()
WidgetCenter.shared.reloadAllTimelines()
// Flush pending analytics events
AnalyticsManager.shared.flush()
// Lock the app when going to background
authManager.lock()
}
if newPhase == .active {
UNUserNotificationCenter.current().setBadgeCount(0)
// Authenticate if locked - this must happen immediately on main thread
if authManager.isLockEnabled && !authManager.isUnlocked {
Task {
await authManager.authenticate()
}
}
// Defer all non-critical foreground work to avoid blocking UI
Task(priority: .utility) {
// Refresh from disk to pick up widget/watch changes
DataController.shared.refreshFromDisk()
// Fill in any missing dates (moved from AppDelegate)
DataController.shared.fillInMissingDates()
// Clean up any duplicate entries (after backfill so backfill dupes are caught)
DataController.shared.removeDuplicates()
// Reschedule notifications for new title
LocalNotification.rescheduleNotifiations()
// Update super properties on foreground
AnalyticsManager.shared.updateSuperProperties()
}
// Defer Live Activity scheduling (heavy DB operations)
Task(priority: .utility) {
await LiveActivityScheduler.shared.scheduleBasedOnCurrentTime()
}
// Catch up on side effects from widget/watch votes
Task(priority: .utility) {
await MoodLogger.shared.processPendingSideEffects()
}
// Check subscription status (network call) - throttled
Task(priority: .background) {
await iapManager.checkSubscriptionStatus()
await iapManager.trackSubscriptionAnalytics(source: "app_foreground")
}
}
}
}
private func handleDeepLink(_ url: URL) {
if url.scheme == "reflect" && url.host == "subscribe" {
showSubscriptionFromWidget = true
}
}
}