diff --git a/Reflect Watch App/ContentView.swift b/Reflect Watch App/ContentView.swift index f4d1c79..31a03ef 100644 --- a/Reflect Watch App/ContentView.swift +++ b/Reflect Watch App/ContentView.swift @@ -22,8 +22,9 @@ struct ContentView: View { // Show voting UI VStack(spacing: 8) { Text("How do you feel?") - .font(.system(size: 16, weight: .medium)) + .font(.headline) .foregroundColor(.secondary) + .accessibilityAddTraits(.isHeader) // Top row: Great, Good, Average HStack(spacing: 8) { @@ -87,11 +88,14 @@ struct AlreadyRatedView: View { VStack(spacing: 12) { Text(mood.watchEmoji) .font(.system(size: 50)) + .accessibilityHidden(true) Text("Logged!") - .font(.system(size: 18, weight: .semibold)) + .font(.title3.weight(.semibold)) .foregroundColor(.secondary) } + .accessibilityElement(children: .combine) + .accessibilityLabel(String(localized: "\(mood.strValue) mood logged")) } } @@ -104,7 +108,7 @@ struct MoodButton: View { var body: some View { Button(action: action) { Text(mood.watchEmoji) - .font(.system(size: 28)) + .font(.title2) .frame(maxWidth: .infinity) .frame(height: 50) .background(mood.watchColor.opacity(0.3)) @@ -112,6 +116,8 @@ struct MoodButton: View { } .buttonStyle(.plain) .accessibilityIdentifier(AccessibilityID.Watch.moodButton(mood.strValue)) + .accessibilityLabel(String(localized: "Log \(mood.strValue) mood")) + .accessibilityHint(String(localized: "Double tap to log your mood as \(mood.strValue)")) } } diff --git a/ReflectWidget/ReflectLiveActivity.swift b/ReflectWidget/ReflectLiveActivity.swift index 81fe316..bf290e3 100644 --- a/ReflectWidget/ReflectLiveActivity.swift +++ b/ReflectWidget/ReflectLiveActivity.swift @@ -24,9 +24,12 @@ struct MoodStreakLiveActivity: Widget { HStack(spacing: 8) { Image(systemName: "flame.fill") .foregroundColor(.orange) + .accessibilityHidden(true) Text("\(context.state.currentStreak)") .font(.title2.bold()) } + .accessibilityElement(children: .combine) + .accessibilityLabel(String(localized: "\(context.state.currentStreak) day streak")) } DynamicIslandExpandedRegion(.trailing) { @@ -34,6 +37,7 @@ struct MoodStreakLiveActivity: Widget { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) .font(.title2) + .accessibilityLabel(String(localized: "Mood logged today")) } else { Text("Log now") .font(.caption) @@ -56,20 +60,25 @@ struct MoodStreakLiveActivity: Widget { Circle() .fill(Color(hex: context.state.lastMoodColor)) .frame(width: 20, height: 20) + .accessibilityHidden(true) Text("Today: \(context.state.lastMoodLogged)") .font(.subheadline) } + .accessibilityElement(children: .combine) } } } compactLeading: { Image(systemName: "flame.fill") .foregroundColor(.orange) + .accessibilityLabel(String(localized: "Streak")) } compactTrailing: { Text("\(context.state.currentStreak)") .font(.caption.bold()) + .accessibilityLabel(String(localized: "\(context.state.currentStreak) days")) } minimal: { Image(systemName: "flame.fill") .foregroundColor(.orange) + .accessibilityLabel(String(localized: "Mood streak")) } } } @@ -87,12 +96,15 @@ struct MoodStreakLockScreenView: View { Image(systemName: "flame.fill") .font(.title) .foregroundColor(.orange) + .accessibilityHidden(true) Text("\(context.state.currentStreak)") .font(.title.bold()) Text("day streak") .font(.caption) .foregroundColor(.secondary) } + .accessibilityElement(children: .combine) + .accessibilityLabel(String(localized: "\(context.state.currentStreak) day streak")) Divider() .frame(height: 50) @@ -104,6 +116,7 @@ struct MoodStreakLockScreenView: View { Circle() .fill(Color(hex: context.state.lastMoodColor)) .frame(width: 24, height: 24) + .accessibilityHidden(true) VStack(alignment: .leading) { Text("Today's mood") .font(.caption) @@ -112,6 +125,7 @@ struct MoodStreakLockScreenView: View { .font(.headline) } } + .accessibilityElement(children: .combine) } else { VStack(alignment: .leading) { Text(context.state.currentStreak > 0 ? "Don't break your streak!" : "Start your streak!") diff --git a/ReflectWidget/ReflectTimelineWidget.swift b/ReflectWidget/ReflectTimelineWidget.swift index c293c39..d9a5190 100644 --- a/ReflectWidget/ReflectTimelineWidget.swift +++ b/ReflectWidget/ReflectTimelineWidget.swift @@ -82,6 +82,8 @@ struct SmallWidgetView: View { return f } + private var isSampleData: Bool + init(entry: Provider.Entry) { self.entry = entry let realData = TimeLineCreator.createViews(daysBack: 2) @@ -89,6 +91,7 @@ struct SmallWidgetView: View { let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable() return view.color != moodTint.color(forMood: .missing) } + isSampleData = !hasRealData todayView = hasRealData ? realData.first : TimeLineCreator.createSampleViews(count: 1).first } @@ -98,6 +101,13 @@ struct SmallWidgetView: View { VotingView(family: .systemSmall, promptText: entry.promptText, hasSubscription: entry.hasSubscription) } else if let today = todayView { VStack(spacing: 0) { + if isSampleData { + Text(String(localized: "Log your first mood!")) + .font(.caption2) + .foregroundStyle(.secondary) + .padding(.top, 8) + } + Spacer() // Large mood icon @@ -152,6 +162,8 @@ struct MediumWidgetView: View { return f } + private var isSampleData: Bool + init(entry: Provider.Entry) { self.entry = entry let realData = Array(TimeLineCreator.createViews(daysBack: 6).prefix(5)) @@ -159,6 +171,7 @@ struct MediumWidgetView: View { let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable() return view.color != moodTint.color(forMood: .missing) } + isSampleData = !hasRealData timeLineView = hasRealData ? realData : TimeLineCreator.createSampleViews(count: 5) } @@ -183,11 +196,19 @@ struct MediumWidgetView: View { Text("Last 5 Days") .font(.subheadline.weight(.semibold)) .foregroundStyle(.primary) - Text("ยท") - .foregroundStyle(.secondary) - Text(headerDateRange) - .font(.caption) - .foregroundStyle(.secondary) + if isSampleData { + Text("ยท") + .foregroundStyle(.secondary) + Text(String(localized: "Log your first mood!")) + .font(.caption) + .foregroundStyle(.secondary) + } else { + Text("ยท") + .foregroundStyle(.secondary) + Text(headerDateRange) + .font(.caption) + .foregroundStyle(.secondary) + } Spacer() } .padding(.horizontal, 14) @@ -264,6 +285,8 @@ struct LargeWidgetView: View { !entry.hasVotedToday } + private var isSampleData: Bool + init(entry: Provider.Entry) { self.entry = entry let realData = Array(TimeLineCreator.createViews(daysBack: 11).prefix(10)) @@ -271,6 +294,7 @@ struct LargeWidgetView: View { let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable() return view.color != moodTint.color(forMood: .missing) } + isSampleData = !hasRealData timeLineView = hasRealData ? realData : TimeLineCreator.createSampleViews(count: 10) } @@ -301,7 +325,7 @@ struct LargeWidgetView: View { Text("Last 10 Days") .font(.subheadline.weight(.semibold)) .foregroundStyle(.primary) - Text(headerDateRange) + Text(isSampleData ? String(localized: "Log your first mood!") : headerDateRange) .font(.caption2) .foregroundStyle(.secondary) } diff --git a/ReflectWidget/ReflectVoteWidget.swift b/ReflectWidget/ReflectVoteWidget.swift index f64cdda..9239104 100644 --- a/ReflectWidget/ReflectVoteWidget.swift +++ b/ReflectWidget/ReflectVoteWidget.swift @@ -158,10 +158,13 @@ struct VotedStatsView: View { Circle() .fill(moodTint.color(forMood: mood)) .frame(width: 8, height: 8) + .accessibilityHidden(true) Text("\(count)") .font(.caption2) .foregroundStyle(.secondary) } + .accessibilityElement(children: .combine) + .accessibilityLabel("\(count) \(mood.strValue)") } } } diff --git a/ReflectWidget/WidgetSharedViews.swift b/ReflectWidget/WidgetSharedViews.swift index 7874f6c..1e7ed31 100644 --- a/ReflectWidget/WidgetSharedViews.swift +++ b/ReflectWidget/WidgetSharedViews.swift @@ -58,8 +58,8 @@ struct VotingView: View { VStack(spacing: 0) { // Top 50%: Text left-aligned, vertically centered HStack { - Text(hasSubscription ? promptText : "Subscribe to track your mood") - .font(.system(size: 20, weight: .semibold)) + Text(hasSubscription ? promptText : String(localized: "Subscribe to track your mood")) + .font(.title3.weight(.semibold)) .foregroundStyle(.primary) .multilineTextAlignment(.leading) .lineLimit(2) @@ -159,8 +159,8 @@ struct LargeVotingView: View { GeometryReader { geo in VStack(spacing: 0) { // Top 33%: Title centered - Text(hasSubscription ? promptText : "Subscribe to track your mood") - .font(.system(size: 24, weight: .semibold)) + Text(hasSubscription ? promptText : String(localized: "Subscribe to track your mood")) + .font(.title2.weight(.semibold)) .foregroundStyle(.primary) .multilineTextAlignment(.center) .lineLimit(2) diff --git a/Shared/BGTask.swift b/Shared/BGTask.swift index a01c74d..d0adb5a 100644 --- a/Shared/BGTask.swift +++ b/Shared/BGTask.swift @@ -51,7 +51,9 @@ class BGTask { do { try BGTaskScheduler.shared.submit(request) } catch { + #if DEBUG print("Could not schedule weather retry: \(error)") + #endif } } @@ -67,7 +69,9 @@ class BGTask { do { try BGTaskScheduler.shared.submit(request) } catch { + #if DEBUG print("Could not schedule image fetch: \(error)") + #endif } } } diff --git a/Shared/Color+Codable.swift b/Shared/Color+Codable.swift index 324166e..bedfda7 100644 --- a/Shared/Color+Codable.swift +++ b/Shared/Color+Codable.swift @@ -134,7 +134,6 @@ extension Color { } extension Color: @retroactive RawRepresentable { - // TODO: Sort out alpha public init?(rawValue: Int) { let red = Double((rawValue & 0xFF0000) >> 16) / 0xFF let green = Double((rawValue & 0x00FF00) >> 8) / 0xFF diff --git a/Shared/IAPManager.swift b/Shared/IAPManager.swift index 073a4df..d7f1d5c 100644 --- a/Shared/IAPManager.swift +++ b/Shared/IAPManager.swift @@ -307,8 +307,16 @@ class IAPManager: ObservableObject { // Get renewal info if let product = currentProduct, - let subscription = product.subscription, - let statuses = try? await subscription.status { + let subscription = product.subscription { + let statuses: [Product.SubscriptionInfo.Status] + do { + statuses = try await subscription.status + } catch { + AppLogger.iap.error("Failed to fetch subscription status for \(product.id): \(error)") + // Fallback handled below + state = .subscribed(expirationDate: transaction.expirationDate, willAutoRenew: false) + return true + } var hadVerifiedStatus = false for status in statuses { diff --git a/Shared/LocalNotification.swift b/Shared/LocalNotification.swift index a77882d..3e8c581 100644 --- a/Shared/LocalNotification.swift +++ b/Shared/LocalNotification.swift @@ -69,11 +69,15 @@ class LocalNotification { let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: trigger) UNUserNotificationCenter.current().add(request) { (error : Error?) in if let theError = error { + #if DEBUG print(theError.localizedDescription) + #endif } } case .failure(let error): + #if DEBUG print(error) + #endif // Todo: show enable this break } diff --git a/Shared/Models/UserDefaultsStore.swift b/Shared/Models/UserDefaultsStore.swift index ccb7c5d..efd8336 100644 --- a/Shared/Models/UserDefaultsStore.swift +++ b/Shared/Models/UserDefaultsStore.swift @@ -6,6 +6,7 @@ // import Foundation +import os.log enum VotingLayoutStyle: Int, CaseIterable { case horizontal = 0 // Current: 5 buttons in a row @@ -177,6 +178,8 @@ enum DayViewStyle: Int, CaseIterable { } } +private let userDefaultsLogger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.88oakapps.reflect", category: "UserDefaults") + class UserDefaultsStore { enum Keys: String { case savedOnboardingData @@ -226,15 +229,18 @@ class UserDefaultsStore { } // Decode and cache - if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.savedOnboardingData.rawValue) as? Data, - let model = try? JSONDecoder().decode(OnboardingData.self, from: data) { - cachedOnboardingData = model - return model - } else { - let defaultData = OnboardingData() - cachedOnboardingData = defaultData - return defaultData + if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.savedOnboardingData.rawValue) as? Data { + do { + let model = try JSONDecoder().decode(OnboardingData.self, from: data) + cachedOnboardingData = model + return model + } catch { + userDefaultsLogger.error("Failed to decode onboarding data: \(error)") + } } + let defaultData = OnboardingData() + cachedOnboardingData = defaultData + return defaultData } /// Invalidate cached onboarding data (call when data might have changed externally) @@ -251,7 +257,7 @@ class UserDefaultsStore { let data = try JSONEncoder().encode(onboardingData) GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.savedOnboardingData.rawValue) } catch { - print("Error saving onboarding: \(error)") + userDefaultsLogger.error("Failed to encode onboarding data: \(error)") } // Re-cache the saved data @@ -314,28 +320,38 @@ class UserDefaultsStore { } static func getCustomWidgets() -> [CustomWidgetModel] { - if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.customWidget.rawValue) as? Data, - let model = try? JSONDecoder().decode([CustomWidgetModel].self, from: data) { - return model - } else { - GroupUserDefaults.groupDefaults.removeObject(forKey: UserDefaultsStore.Keys.customWidget.rawValue) - - let widget = CustomWidgetModel.randomWidget - widget.isSaved = true - let widgets = [widget] - - guard let data = try? JSONEncoder().encode(widgets) else { - return widgets - } - GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.customWidget.rawValue) - - if let savedData = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.customWidget.rawValue) as? Data, - let models = try? JSONDecoder().decode([CustomWidgetModel].self, from: savedData) { - return models.sorted { $0.createdDate < $1.createdDate } - } else { - return widgets + if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.customWidget.rawValue) as? Data { + do { + let model = try JSONDecoder().decode([CustomWidgetModel].self, from: data) + return model + } catch { + userDefaultsLogger.error("Failed to decode custom widgets: \(error)") } } + + GroupUserDefaults.groupDefaults.removeObject(forKey: UserDefaultsStore.Keys.customWidget.rawValue) + + let widget = CustomWidgetModel.randomWidget + widget.isSaved = true + let widgets = [widget] + + do { + let data = try JSONEncoder().encode(widgets) + GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.customWidget.rawValue) + } catch { + userDefaultsLogger.error("Failed to encode default custom widgets: \(error)") + return widgets + } + + if let savedData = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.customWidget.rawValue) as? Data { + do { + let models = try JSONDecoder().decode([CustomWidgetModel].self, from: savedData) + return models.sorted { $0.createdDate < $1.createdDate } + } catch { + userDefaultsLogger.error("Failed to decode saved custom widgets: \(error)") + } + } + return widgets } @discardableResult @@ -366,7 +382,7 @@ class UserDefaultsStore { let data = try JSONEncoder().encode(existingWidgets) GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.customWidget.rawValue) } catch { - print("Error saving custom widget: \(error)") + userDefaultsLogger.error("Failed to encode custom widget for save: \(error)") } return UserDefaultsStore.getCustomWidgets() } @@ -396,7 +412,7 @@ class UserDefaultsStore { let data = try JSONEncoder().encode(existingWidgets) GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.customWidget.rawValue) } catch { - print("Error deleting custom widget: \(error)") + userDefaultsLogger.error("Failed to encode custom widgets for delete: \(error)") } return UserDefaultsStore.getCustomWidgets() } @@ -407,7 +423,7 @@ class UserDefaultsStore { let model = try JSONDecoder().decode(SavedMoodTint.self, from: data) return model } catch { - print(error) + userDefaultsLogger.error("Failed to decode custom mood tint: \(error)") } } return SavedMoodTint() @@ -428,7 +444,7 @@ class UserDefaultsStore { let data = try JSONEncoder().encode(customTint) GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.customMoodTint.rawValue) } catch { - print("Error saving custom mood tint: \(error)") + userDefaultsLogger.error("Failed to encode custom mood tint: \(error)") } return UserDefaultsStore.getCustomMoodTint() } diff --git a/Shared/MoodStreakActivity.swift b/Shared/MoodStreakActivity.swift index c5d19ae..bb0fcd1 100644 --- a/Shared/MoodStreakActivity.swift +++ b/Shared/MoodStreakActivity.swift @@ -37,7 +37,9 @@ class LiveActivityManager: ObservableObject { // Start a mood streak Live Activity func startStreakActivity(streak: Int, lastMood: Mood?, hasLoggedToday: Bool) { guard ActivityAuthorizationInfo().areActivitiesEnabled else { + #if DEBUG print("Live Activities not enabled") + #endif return } @@ -76,7 +78,9 @@ class LiveActivityManager: ObservableObject { ) currentActivity = activity } catch { + #if DEBUG print("Error starting Live Activity: \(error)") + #endif } } @@ -257,23 +261,31 @@ class LiveActivityScheduler: ObservableObject { invalidateTimers() guard ActivityAuthorizationInfo().areActivitiesEnabled else { + #if DEBUG print("[LiveActivity] Live Activities not enabled by user") + #endif return } let now = Date() guard let startTime = getStartTime(), let endTime = getEndTime() else { + #if DEBUG print("[LiveActivity] No rating time configured - skipping") + #endif return } let hasRated = hasRatedToday() + #if DEBUG print("[LiveActivity] Schedule check - now: \(now), start: \(startTime), end: \(endTime), hasRated: \(hasRated)") + #endif // If user has already rated today, don't show activity - schedule for next day if hasRated { + #if DEBUG print("[LiveActivity] User already rated today - scheduling for next day") + #endif scheduleForNextDay() return } @@ -281,7 +293,9 @@ class LiveActivityScheduler: ObservableObject { // Check if we're within the activity window (rating time to 5 hrs after) if now >= startTime && now <= endTime { // Start activity immediately + #if DEBUG print("[LiveActivity] Within window - starting activity now") + #endif let streak = calculateStreak() LiveActivityManager.shared.startStreakActivity(streak: streak, lastMood: getTodaysMood(), hasLoggedToday: false) @@ -289,12 +303,16 @@ class LiveActivityScheduler: ObservableObject { scheduleEnd(at: endTime) } else if now < startTime { // Schedule start for later today + #if DEBUG print("[LiveActivity] Before window - scheduling start for \(startTime)") + #endif scheduleStart(at: startTime) scheduleEnd(at: endTime) } else { // Past the window for today, schedule for tomorrow + #if DEBUG print("[LiveActivity] Past window - scheduling for tomorrow") + #endif scheduleForNextDay() } } diff --git a/Shared/ReflectApp.swift b/Shared/ReflectApp.swift index 308c4dc..9763f9b 100644 --- a/Shared/ReflectApp.swift +++ b/Shared/ReflectApp.swift @@ -12,6 +12,10 @@ 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 @@ -83,7 +87,7 @@ struct ReflectApp: App { } if let url = AppDelegate.pendingDeepLinkURL { AppDelegate.pendingDeepLinkURL = nil - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + DispatchQueue.main.asyncAfter(deadline: .now() + AnimationConstants.deepLinkHandlingDelay) { handleDeepLink(url) } } diff --git a/Shared/ReflectTips.swift b/Shared/ReflectTips.swift index 0593801..f00faad 100644 --- a/Shared/ReflectTips.swift +++ b/Shared/ReflectTips.swift @@ -238,6 +238,10 @@ class ReflectTipsManager: ObservableObject { // MARK: - View Modifier for Easy Integration struct ReflectTipModifier: ViewModifier { + private enum AnimationConstants { + static let tipPresentationDelay: TimeInterval = 0.5 + } + let tip: any ReflectTip let gradientColors: [Color] @@ -254,7 +258,7 @@ struct ReflectTipModifier: ViewModifier { // Delay tip presentation to ensure view hierarchy is fully established // This prevents "presenting from detached view controller" errors - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + DispatchQueue.main.asyncAfter(deadline: .now() + AnimationConstants.tipPresentationDelay) { if ReflectTipsManager.shared.shouldShowTip(tip) { showSheet = true } diff --git a/Shared/Services/BiometricAuthManager.swift b/Shared/Services/BiometricAuthManager.swift index 3834f98..69e1d3c 100644 --- a/Shared/Services/BiometricAuthManager.swift +++ b/Shared/Services/BiometricAuthManager.swift @@ -102,7 +102,9 @@ class BiometricAuthManager: ObservableObject { } return success } catch { + #if DEBUG print("Authentication failed: \(error.localizedDescription)") + #endif AnalyticsManager.shared.track(.biometricUnlockFailed(error: error.localizedDescription)) // If biometrics failed, try device passcode as fallback @@ -126,7 +128,9 @@ class BiometricAuthManager: ObservableObject { isUnlocked = success return success } catch { + #if DEBUG print("Passcode authentication failed: \(error.localizedDescription)") + #endif return false } } @@ -146,7 +150,9 @@ class BiometricAuthManager: ObservableObject { // Only allow enabling if biometrics are available guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { + #if DEBUG print("Biometric authentication not available: \(error?.localizedDescription ?? "Unknown")") + #endif return false } @@ -164,7 +170,9 @@ class BiometricAuthManager: ObservableObject { return success } catch { + #if DEBUG print("Failed to enable lock: \(error.localizedDescription)") + #endif return false } } diff --git a/Shared/Services/ExportService.swift b/Shared/Services/ExportService.swift index c4b9574..83df47e 100644 --- a/Shared/Services/ExportService.swift +++ b/Shared/Services/ExportService.swift @@ -87,7 +87,9 @@ class ExportService { trackDataExported(format: "csv", count: entries.count) return tempURL } catch { + #if DEBUG print("ExportService: Failed to write CSV: \(error)") + #endif return nil } } @@ -177,7 +179,9 @@ class ExportService { try data.write(to: tempURL) return tempURL } catch { + #if DEBUG print("ExportService: Failed to write PDF: \(error)") + #endif return nil } } diff --git a/Shared/Services/FoundationModelsInsightService.swift b/Shared/Services/FoundationModelsInsightService.swift index ca0381e..c0f1c8b 100644 --- a/Shared/Services/FoundationModelsInsightService.swift +++ b/Shared/Services/FoundationModelsInsightService.swift @@ -7,6 +7,7 @@ import Foundation import FoundationModels +import os.log /// Error types for insight generation enum InsightGenerationError: Error, LocalizedError { @@ -244,9 +245,7 @@ class FoundationModelsInsightService: ObservableObject { return insights } catch { // Log detailed error for debugging - print("โŒ AI Insight generation failed for '\(periodName)': \(error)") - print(" Error type: \(type(of: error))") - print(" Localized: \(error.localizedDescription)") + AppLogger.ai.error("AI Insight generation failed for '\(periodName)': \(error)") lastError = .generationFailed(underlying: error) throw lastError! diff --git a/Shared/Services/HealthService.swift b/Shared/Services/HealthService.swift index 0bcfa4a..46b1550 100644 --- a/Shared/Services/HealthService.swift +++ b/Shared/Services/HealthService.swift @@ -71,7 +71,9 @@ class HealthService: ObservableObject { func requestAuthorization() async -> Bool { guard isAvailable else { + #if DEBUG print("HealthService: HealthKit not available on this device") + #endif return false } @@ -82,7 +84,9 @@ class HealthService: ObservableObject { AnalyticsManager.shared.track(.healthKitAuthorized) return true } catch { + #if DEBUG print("HealthService: Authorization failed: \(error.localizedDescription)") + #endif AnalyticsManager.shared.track(.healthKitAuthFailed(error: error.localizedDescription)) return false } diff --git a/Shared/Services/InsightsExporter.swift b/Shared/Services/InsightsExporter.swift index 3fc5f9c..a25a817 100644 --- a/Shared/Services/InsightsExporter.swift +++ b/Shared/Services/InsightsExporter.swift @@ -8,6 +8,7 @@ #if DEBUG import SwiftUI import UIKit +import os.log /// Exports insights view screenshots for App Store marketing @MainActor @@ -28,7 +29,12 @@ class InsightsExporter { // Clean and create export directory try? FileManager.default.removeItem(at: exportPath) - try? FileManager.default.createDirectory(at: exportPath, withIntermediateDirectories: true) + do { + try FileManager.default.createDirectory(at: exportPath, withIntermediateDirectories: true) + } catch { + AppLogger.export.error("Failed to create insights export directory: \(error)") + return nil + } var totalExported = 0 @@ -95,7 +101,11 @@ class InsightsExporter { if let image = renderer.uiImage { let url = folder.appendingPathComponent("\(name).png") if let data = image.pngData() { - try? data.write(to: url) + do { + try data.write(to: url) + } catch { + AppLogger.export.error("Failed to write insights image '\(name)': \(error)") + } } } } diff --git a/Shared/Services/PhotoManager.swift b/Shared/Services/PhotoManager.swift index 86091cd..76ccc31 100644 --- a/Shared/Services/PhotoManager.swift +++ b/Shared/Services/PhotoManager.swift @@ -92,7 +92,11 @@ class PhotoManager: ObservableObject { let thumbnailURL = thumbnailsDir.appendingPathComponent(filename) if let thumbnail = createThumbnail(from: image), let thumbnailData = thumbnail.jpegData(compressionQuality: 0.6) { - try? thumbnailData.write(to: thumbnailURL) + do { + try thumbnailData.write(to: thumbnailURL) + } catch { + AppLogger.photos.error("Failed to save thumbnail: \(error)") + } } AnalyticsManager.shared.track(.photoAdded) @@ -107,13 +111,21 @@ class PhotoManager: ObservableObject { let filename = "\(id.uuidString).jpg" let fullURL = photosDir.appendingPathComponent(filename) - guard FileManager.default.fileExists(atPath: fullURL.path), - let data = try? Data(contentsOf: fullURL), - let image = UIImage(data: data) else { + guard FileManager.default.fileExists(atPath: fullURL.path) else { return nil } - return image + do { + let data = try Data(contentsOf: fullURL) + guard let image = UIImage(data: data) else { + AppLogger.photos.error("Failed to create UIImage from photo data: \(id)") + return nil + } + return image + } catch { + AppLogger.photos.error("Failed to read photo data for \(id): \(error)") + return nil + } } func loadThumbnail(id: UUID) -> UIImage? { @@ -123,10 +135,15 @@ class PhotoManager: ObservableObject { let thumbnailURL = thumbnailsDir.appendingPathComponent(filename) // Try thumbnail first - if FileManager.default.fileExists(atPath: thumbnailURL.path), - let data = try? Data(contentsOf: thumbnailURL), - let image = UIImage(data: data) { - return image + if FileManager.default.fileExists(atPath: thumbnailURL.path) { + do { + let data = try Data(contentsOf: thumbnailURL) + if let image = UIImage(data: data) { + return image + } + } catch { + AppLogger.photos.error("Failed to read thumbnail data for \(id): \(error)") + } } // Fall back to full image if thumbnail doesn't exist @@ -159,7 +176,11 @@ class PhotoManager: ObservableObject { // Delete thumbnail if FileManager.default.fileExists(atPath: thumbnailURL.path) { - try? FileManager.default.removeItem(at: thumbnailURL) + do { + try FileManager.default.removeItem(at: thumbnailURL) + } catch { + AppLogger.photos.error("Failed to delete thumbnail: \(error)") + } } if success { @@ -197,8 +218,13 @@ class PhotoManager: ObservableObject { var totalPhotoCount: Int { guard let photosDir = photosDirectory else { return 0 } - let files = try? FileManager.default.contentsOfDirectory(atPath: photosDir.path) - return files?.filter { $0.hasSuffix(".jpg") }.count ?? 0 + do { + let files = try FileManager.default.contentsOfDirectory(atPath: photosDir.path) + return files.filter { $0.hasSuffix(".jpg") }.count + } catch { + AppLogger.photos.error("Failed to list photos directory: \(error)") + return 0 + } } var totalStorageUsed: Int64 { diff --git a/Shared/Services/SharingScreenshotExporter.swift b/Shared/Services/SharingScreenshotExporter.swift index ad9675a..e0ccc0f 100644 --- a/Shared/Services/SharingScreenshotExporter.swift +++ b/Shared/Services/SharingScreenshotExporter.swift @@ -8,6 +8,7 @@ #if DEBUG import SwiftUI import UIKit +import os.log /// Exports sharing template screenshots for App Store marketing @MainActor @@ -21,13 +22,23 @@ class SharingScreenshotExporter { // Clean and create export directory try? FileManager.default.removeItem(at: exportPath) - try? FileManager.default.createDirectory(at: exportPath, withIntermediateDirectories: true) + do { + try FileManager.default.createDirectory(at: exportPath, withIntermediateDirectories: true) + } catch { + AppLogger.export.error("Failed to create sharing export directory: \(error)") + return nil + } // Create subdirectories let origDir = exportPath.appendingPathComponent("originals", isDirectory: true) let varDir = exportPath.appendingPathComponent("variations", isDirectory: true) - try? FileManager.default.createDirectory(at: origDir, withIntermediateDirectories: true) - try? FileManager.default.createDirectory(at: varDir, withIntermediateDirectories: true) + do { + try FileManager.default.createDirectory(at: origDir, withIntermediateDirectories: true) + try FileManager.default.createDirectory(at: varDir, withIntermediateDirectories: true) + } catch { + AppLogger.export.error("Failed to create sharing subdirectories: \(error)") + return nil + } var totalExported = 0 let distantPast = Date(timeIntervalSince1970: 0) @@ -167,7 +178,7 @@ class SharingScreenshotExporter { try data.write(to: url) return true } catch { - print("Failed to save \(name): \(error)") + AppLogger.export.error("Failed to save sharing screenshot '\(name)': \(error)") } } return false diff --git a/Shared/Services/WatchExporter.swift b/Shared/Services/WatchExporter.swift index b1d0aed..2754158 100644 --- a/Shared/Services/WatchExporter.swift +++ b/Shared/Services/WatchExporter.swift @@ -9,6 +9,7 @@ #if DEBUG import SwiftUI import UIKit +import os.log /// Exports watch view previews to PNG files for App Store screenshots @MainActor @@ -76,7 +77,12 @@ class WatchExporter { // Clean and create export directory try? FileManager.default.removeItem(at: exportPath) - try? FileManager.default.createDirectory(at: exportPath, withIntermediateDirectories: true) + do { + try FileManager.default.createDirectory(at: exportPath, withIntermediateDirectories: true) + } catch { + AppLogger.export.error("Failed to create watch export directory: \(error)") + return nil + } var totalExported = 0 @@ -85,7 +91,12 @@ class WatchExporter { for iconOption in allIcons { let folderName = "\(tintOption.name)_\(iconOption.name)" let variantPath = exportPath.appendingPathComponent(folderName, isDirectory: true) - try? FileManager.default.createDirectory(at: variantPath, withIntermediateDirectories: true) + do { + try FileManager.default.createDirectory(at: variantPath, withIntermediateDirectories: true) + } catch { + AppLogger.export.error("Failed to create watch variant directory '\(folderName)': \(error)") + continue + } let config = WatchExportConfig( moodTint: tintOption.tint, @@ -242,7 +253,11 @@ class WatchExporter { if let image = renderer.uiImage { let url = folder.appendingPathComponent("\(name).png") if let data = image.pngData() { - try? data.write(to: url) + do { + try data.write(to: url) + } catch { + AppLogger.export.error("Failed to write watch image '\(name)': \(error)") + } } } } diff --git a/Shared/Services/WidgetExporter.swift b/Shared/Services/WidgetExporter.swift index 05ce11c..d2ae186 100644 --- a/Shared/Services/WidgetExporter.swift +++ b/Shared/Services/WidgetExporter.swift @@ -9,6 +9,7 @@ #if DEBUG import SwiftUI import UIKit +import os.log /// Exports widget previews to PNG files for App Store screenshots @MainActor @@ -76,7 +77,12 @@ class WidgetExporter { // Clean and create export directory try? FileManager.default.removeItem(at: exportPath) - try? FileManager.default.createDirectory(at: exportPath, withIntermediateDirectories: true) + do { + try FileManager.default.createDirectory(at: exportPath, withIntermediateDirectories: true) + } catch { + AppLogger.export.error("Failed to create widget export directory: \(error)") + return nil + } var totalExported = 0 @@ -85,7 +91,12 @@ class WidgetExporter { for iconOption in allIcons { let folderName = "\(tintOption.name)_\(iconOption.name)" let variantPath = exportPath.appendingPathComponent(folderName, isDirectory: true) - try? FileManager.default.createDirectory(at: variantPath, withIntermediateDirectories: true) + do { + try FileManager.default.createDirectory(at: variantPath, withIntermediateDirectories: true) + } catch { + AppLogger.export.error("Failed to create variant directory '\(folderName)': \(error)") + continue + } let config = WidgetExportConfig( moodTint: tintOption.tint, @@ -155,7 +166,12 @@ class WidgetExporter { let exportPath = documentsPath.appendingPathComponent("WidgetExports_Current", isDirectory: true) try? FileManager.default.removeItem(at: exportPath) - try? FileManager.default.createDirectory(at: exportPath, withIntermediateDirectories: true) + do { + try FileManager.default.createDirectory(at: exportPath, withIntermediateDirectories: true) + } catch { + AppLogger.export.error("Failed to create current config export directory: \(error)") + return nil + } let config = WidgetExportConfig( moodTint: UserDefaultsStore.moodTintable(), @@ -177,7 +193,12 @@ class WidgetExporter { // Clean and create export directory try? FileManager.default.removeItem(at: exportPath) - try? FileManager.default.createDirectory(at: exportPath, withIntermediateDirectories: true) + do { + try FileManager.default.createDirectory(at: exportPath, withIntermediateDirectories: true) + } catch { + AppLogger.export.error("Failed to create voting layout export directory: \(error)") + return nil + } var totalExported = 0 @@ -186,7 +207,12 @@ class WidgetExporter { for iconOption in allIcons { let folderName = "\(tintOption.name)_\(iconOption.name)" let variantPath = exportPath.appendingPathComponent(folderName, isDirectory: true) - try? FileManager.default.createDirectory(at: variantPath, withIntermediateDirectories: true) + do { + try FileManager.default.createDirectory(at: variantPath, withIntermediateDirectories: true) + } catch { + AppLogger.export.error("Failed to create voting variant directory '\(folderName)': \(error)") + continue + } let config = WidgetExportConfig( moodTint: tintOption.tint, @@ -372,7 +398,11 @@ class WidgetExporter { if let image = renderer.uiImage { let url = folder.appendingPathComponent("\(name).png") if let data = image.pngData() { - try? data.write(to: url) + do { + try data.write(to: url) + } catch { + AppLogger.export.error("Failed to write widget image '\(name)': \(error)") + } } } } @@ -384,7 +414,11 @@ class WidgetExporter { if let image = renderer.uiImage { let url = folder.appendingPathComponent("\(name).png") if let data = image.pngData() { - try? data.write(to: url) + do { + try data.write(to: url) + } catch { + AppLogger.export.error("Failed to write live activity image '\(name)': \(error)") + } } } } diff --git a/Shared/Views/CelebrationAnimations.swift b/Shared/Views/CelebrationAnimations.swift index dee933e..08bb848 100644 --- a/Shared/Views/CelebrationAnimations.swift +++ b/Shared/Views/CelebrationAnimations.swift @@ -305,6 +305,12 @@ struct FlipRevealAnimation: View { struct ShatterReformAnimation: View { let mood: Mood + private enum AnimationConstants { + static let shatterPhaseDuration: TimeInterval = 0.6 + static let checkmarkAppearDelay: TimeInterval = 1.1 + static let fadeOutDelay: TimeInterval = 1.8 + } + @State private var shardOffsets: [CGSize] = [] @State private var shardRotations: [Double] = [] @State private var shardOpacities: [Double] = [] @@ -354,7 +360,7 @@ struct ShatterReformAnimation: View { // Phase 2: Converge to center and fade Task { @MainActor in - try? await Task.sleep(for: .seconds(0.6)) + try? await Task.sleep(for: .seconds(AnimationConstants.shatterPhaseDuration)) phase = .reform withAnimation(.easeInOut(duration: 0.5)) { for i in 0.. $1.rawValue }).first?.rawValue) ?? 0 { Divider() } diff --git a/Shared/Views/CustomizeView/SubViews/PersonalityPackPickerView.swift b/Shared/Views/CustomizeView/SubViews/PersonalityPackPickerView.swift index a17fd5d..369bc28 100644 --- a/Shared/Views/CustomizeView/SubViews/PersonalityPackPickerView.swift +++ b/Shared/Views/CustomizeView/SubViews/PersonalityPackPickerView.swift @@ -47,6 +47,8 @@ struct PersonalityPackPickerView: View { LocalNotification.rescheduleNotifiations() // } } + .accessibilityAddTraits(.isButton) + .accessibilityLabel(String(localized: "Select \(aPack.title()) personality pack")) // .blur(radius: aPack.rawValue == PersonalityPack.Rude.rawValue && !showNSFW ? 5 : 0) .alert(isPresented: $showOver18Alert) { let primaryButton = Alert.Button.default(Text(String(localized: "customize_view_over18alert_ok"))) { diff --git a/Shared/Views/CustomizeView/SubViews/ShapePickerView.swift b/Shared/Views/CustomizeView/SubViews/ShapePickerView.swift index fcd15e1..34551e4 100644 --- a/Shared/Views/CustomizeView/SubViews/ShapePickerView.swift +++ b/Shared/Views/CustomizeView/SubViews/ShapePickerView.swift @@ -35,6 +35,8 @@ struct ShapePickerView: View { .onTapGesture { shapeRefreshToggleThing.toggle() } + .accessibilityAddTraits(.isButton) + .accessibilityLabel(String(localized: "Refresh shapes")) } } @@ -51,6 +53,8 @@ struct ShapePickerView: View { shape = ashape AnalyticsManager.shared.track(.moodShapeChanged(shapeId: shape.rawValue)) } + .accessibilityAddTraits(.isButton) + .accessibilityLabel(String(localized: "Select \(String(describing: ashape)) shape")) .contentShape(Rectangle()) .background( RoundedRectangle(cornerRadius: 10, style: .continuous) diff --git a/Shared/Views/DayView/DayViewViewModel.swift b/Shared/Views/DayView/DayViewViewModel.swift index 99146a1..af55d9e 100644 --- a/Shared/Views/DayView/DayViewViewModel.swift +++ b/Shared/Views/DayView/DayViewViewModel.swift @@ -72,7 +72,9 @@ class DayViewViewModel: ObservableObject { public func update(entry: MoodEntryModel, toMood mood: Mood) { if !MoodLogger.shared.updateMood(entryDate: entry.forDate, withMood: mood) { + #if DEBUG print("Failed to update mood entry") + #endif } } diff --git a/Shared/Views/EntryListView.swift b/Shared/Views/EntryListView.swift index 6a735f2..702c3f7 100644 --- a/Shared/Views/EntryListView.swift +++ b/Shared/Views/EntryListView.swift @@ -110,10 +110,12 @@ struct EntryListView: View { if hasNotes { Image(systemName: "note.text") .font(.caption2) + .accessibilityHidden(true) } if hasReflection { Image(systemName: "sparkles") .font(.caption2) + .accessibilityHidden(true) } } .foregroundStyle(.secondary) @@ -134,7 +136,10 @@ struct EntryListView: View { if isMissing { return String(localized: "\(dateString), no mood logged") } else { - return "\(dateString), \(entry.mood.strValue)" + var description = "\(dateString), \(entry.mood.strValue)" + if hasNotes { description += String(localized: ", has notes") } + if hasReflection { description += String(localized: ", has reflection") } + return description } } diff --git a/Shared/Views/GuidedReflectionView.swift b/Shared/Views/GuidedReflectionView.swift index a6a1f93..69c6c62 100644 --- a/Shared/Views/GuidedReflectionView.swift +++ b/Shared/Views/GuidedReflectionView.swift @@ -236,6 +236,8 @@ struct GuidedReflectionView: View { .frame(height: 10) } } + .accessibilityElement(children: .ignore) + .accessibilityLabel(String(localized: "\(draft.steps.filter(\.hasAnswer).count) of \(draft.steps.count) steps completed")) } .accessibilityIdentifier(AccessibilityID.GuidedReflection.progressDots) } diff --git a/Shared/Views/InsightsView/InsightsView.swift b/Shared/Views/InsightsView/InsightsView.swift index b49f6f8..3924b94 100644 --- a/Shared/Views/InsightsView/InsightsView.swift +++ b/Shared/Views/InsightsView/InsightsView.swift @@ -13,6 +13,10 @@ enum InsightsTab: String, CaseIterable { } struct InsightsView: View { + private enum AnimationConstants { + static let refreshDelay: UInt64 = 500_000_000 // 0.5 seconds in nanoseconds + } + @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system @AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default @AppStorage(UserDefaultsStore.Keys.moodImages.rawValue, store: GroupUserDefaults.groupDefaults) private var imagePack: MoodImages = .FontAwesome @@ -40,6 +44,7 @@ struct InsightsView: View { HStack(spacing: 4) { Image(systemName: "sparkles") .font(.caption.weight(.medium)) + .accessibilityHidden(true) Text("AI") .font(.caption.weight(.semibold)) } @@ -148,7 +153,7 @@ struct InsightsView: View { .refreshable { viewModel.refreshInsights() // Small delay to show refresh animation - try? await Task.sleep(nanoseconds: 500_000_000) + try? await Task.sleep(nanoseconds: AnimationConstants.refreshDelay) } .disabled(iapManager.shouldShowPaywall) } @@ -173,6 +178,7 @@ struct InsightsView: View { Image(systemName: "sparkles") .font(.largeTitle) + .accessibilityHidden(true) .foregroundStyle( LinearGradient( colors: [.purple, .blue], @@ -202,6 +208,7 @@ struct InsightsView: View { } label: { HStack { Image(systemName: "sparkles") + .accessibilityHidden(true) Text("Get Personal Insights") } .font(.headline.weight(.bold)) diff --git a/Shared/Views/LockScreenView.swift b/Shared/Views/LockScreenView.swift index e288257..6e0080e 100644 --- a/Shared/Views/LockScreenView.swift +++ b/Shared/Views/LockScreenView.swift @@ -1465,6 +1465,12 @@ struct GlassButton: View { // MARK: - Main Lock Screen View struct LockScreenView: View { + private enum AnimationConstants { + static let contentAppearDuration: TimeInterval = 0.8 + static let contentAppearDelay: TimeInterval = 0.2 + static let authenticationDelay: Int = 800 // milliseconds + } + @Environment(\.colorScheme) private var colorScheme @ObservedObject var authManager: BiometricAuthManager @State private var showError = false @@ -1714,13 +1720,13 @@ struct LockScreenView: View { Text("Unable to verify your identity. Please try again.") } .onAppear { - withAnimation(.easeOut(duration: 0.8).delay(0.2)) { + withAnimation(.easeOut(duration: AnimationConstants.contentAppearDuration).delay(AnimationConstants.contentAppearDelay)) { showContent = true } if !authManager.isUnlocked && !authManager.isAuthenticating { Task { - try? await Task.sleep(for: .milliseconds(800)) + try? await Task.sleep(for: .milliseconds(AnimationConstants.authenticationDelay)) await authManager.authenticate() } } diff --git a/Shared/Views/MonthView/MonthDetailView.swift b/Shared/Views/MonthView/MonthDetailView.swift index 03b0cee..07dedff 100644 --- a/Shared/Views/MonthView/MonthDetailView.swift +++ b/Shared/Views/MonthView/MonthDetailView.swift @@ -58,11 +58,13 @@ struct MonthDetailView: View { .onTapGesture { let impactMed = UIImpactFeedbackGenerator(style: .heavy) impactMed.impactOccurred() - + let _image = self.image self.shareImage.showSheet = true self.shareImage.selectedShareImage = _image } + .accessibilityAddTraits(.isButton) + .accessibilityLabel(String(localized: "Share month")) } .background( theme.currentTheme.secondaryBGColor @@ -161,6 +163,7 @@ struct MonthDetailView: View { showUpdateEntryAlert = true } }) + .accessibilityAddTraits(.isButton) .frame(minWidth: 0, maxWidth: .infinity) } } diff --git a/Shared/Views/NoteEditorView.swift b/Shared/Views/NoteEditorView.swift index 360b6bc..11d768a 100644 --- a/Shared/Views/NoteEditorView.swift +++ b/Shared/Views/NoteEditorView.swift @@ -10,6 +10,10 @@ import PhotosUI struct NoteEditorView: View { + private enum AnimationConstants { + static let keyboardAppearDelay: TimeInterval = 0.5 + } + @Environment(\.dismiss) private var dismiss @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system @@ -57,18 +61,18 @@ struct NoteEditorView: View { } .padding() } - .navigationTitle("Journal Note") + .navigationTitle(String(localized: "Journal Note")) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { + Button(String(localized: "Cancel")) { dismiss() } .accessibilityIdentifier(AccessibilityID.NoteEditor.cancelButton) } ToolbarItem(placement: .confirmationAction) { - Button("Save") { + Button(String(localized: "Save")) { saveNote() } .disabled(isSaving || noteText.count > maxCharacters) @@ -78,14 +82,14 @@ struct NoteEditorView: View { ToolbarItemGroup(placement: .keyboard) { Spacer() - Button("Done") { + Button(String(localized: "Done")) { isTextFieldFocused = false } .accessibilityIdentifier(AccessibilityID.NoteEditor.keyboardDoneButton) } } .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + DispatchQueue.main.asyncAfter(deadline: .now() + AnimationConstants.keyboardAppearDelay) { isTextFieldFocused = true } } @@ -205,12 +209,12 @@ struct EntryDetailView: View { .padding() } .background(Color(.systemGroupedBackground)) - .navigationTitle("Entry Details") + .navigationTitle(String(localized: "Entry Details")) .navigationBarTitleDisplayMode(.inline) .accessibilityIdentifier(AccessibilityID.EntryDetail.sheet) .toolbar { ToolbarItem(placement: .confirmationAction) { - Button("Done") { + Button(String(localized: "Done")) { dismiss() } .accessibilityIdentifier(AccessibilityID.EntryDetail.doneButton) @@ -222,16 +226,16 @@ struct EntryDetailView: View { .sheet(isPresented: $showReflectionFlow) { GuidedReflectionView(entry: entry) } - .alert("Delete Entry", isPresented: $showDeleteConfirmation) { - Button("Delete", role: .destructive) { + .alert(String(localized: "Delete Entry"), isPresented: $showDeleteConfirmation) { + Button(String(localized: "Delete"), role: .destructive) { onDelete() dismiss() } .accessibilityIdentifier(AccessibilityID.EntryDetail.deleteConfirmButton) - Button("Cancel", role: .cancel) { } + Button(String(localized: "Cancel"), role: .cancel) { } .accessibilityIdentifier(AccessibilityID.EntryDetail.deleteCancelButton) } message: { - Text("Are you sure you want to delete this mood entry? This cannot be undone.") + Text(String(localized: "Are you sure you want to delete this mood entry? This cannot be undone.")) } .photosPicker(isPresented: $showPhotoPicker, selection: $selectedPhotoItem, matching: .images) .onChange(of: selectedPhotoItem) { _, newItem in diff --git a/Shared/Views/PhotoPickerView.swift b/Shared/Views/PhotoPickerView.swift index 352c139..34c09a5 100644 --- a/Shared/Views/PhotoPickerView.swift +++ b/Shared/Views/PhotoPickerView.swift @@ -160,7 +160,9 @@ struct PhotoPickerView: View { handleSelectedImage(image) } } catch { + #if DEBUG print("PhotoPickerView: Failed to load image: \(error)") + #endif } } diff --git a/Shared/Views/SampleEntryView.swift b/Shared/Views/SampleEntryView.swift index 3eb0333..74d422c 100644 --- a/Shared/Views/SampleEntryView.swift +++ b/Shared/Views/SampleEntryView.swift @@ -26,6 +26,8 @@ struct SampleEntryView: View { .onTapGesture { sampleListEntry = DataController.shared.generateObjectNotInArray(forDate: Date(), withMood: sampleListEntry.mood.next) } + .accessibilityAddTraits(.isButton) + .accessibilityLabel(String(localized: "Refresh sample entry")) } Spacer() }.padding() diff --git a/Shared/Views/SettingsView/LiveActivityPreviewView.swift b/Shared/Views/SettingsView/LiveActivityPreviewView.swift index c6b4641..5849d85 100644 --- a/Shared/Views/SettingsView/LiveActivityPreviewView.swift +++ b/Shared/Views/SettingsView/LiveActivityPreviewView.swift @@ -324,7 +324,9 @@ struct LiveActivityRecordingView: View { try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) exportPath = outputDir.path + #if DEBUG print("๐Ÿ“ Exporting frames to: \(exportPath)") + #endif let target = targetStreak let outDir = outputDir @@ -359,7 +361,9 @@ struct LiveActivityRecordingView: View { await MainActor.run { exportComplete = true + #if DEBUG print("โœ… Export complete! \(target) frames saved to: \(outPath)") + #endif } } } diff --git a/Shared/Views/SettingsView/SettingsView.swift b/Shared/Views/SettingsView/SettingsView.swift index 0c54640..ffac9f8 100644 --- a/Shared/Views/SettingsView/SettingsView.swift +++ b/Shared/Views/SettingsView/SettingsView.swift @@ -9,6 +9,10 @@ import SwiftUI import UniformTypeIdentifiers import StoreKit +private enum SettingsAnimationConstants { + static let locationPermissionCheckDelay: TimeInterval = 1.0 +} + // MARK: - Settings Content View (for use in SettingsTabView) struct SettingsContentView: View { @EnvironmentObject var authManager: BiometricAuthManager @@ -437,7 +441,9 @@ struct SettingsContentView: View { widgetExportPath = await WidgetExporter.exportAllWidgets() isExportingWidgets = false if let path = widgetExportPath { + #if DEBUG print("๐Ÿ“ธ Widgets exported to: \(path.path)") + #endif openInFilesApp(path) } } @@ -490,7 +496,9 @@ struct SettingsContentView: View { votingLayoutExportPath = await WidgetExporter.exportAllVotingLayouts() isExportingVotingLayouts = false if let path = votingLayoutExportPath { + #if DEBUG print("๐Ÿ“ธ Voting layouts exported to: \(path.path)") + #endif openInFilesApp(path) } } @@ -543,7 +551,9 @@ struct SettingsContentView: View { watchExportPath = await WatchExporter.exportAllWatchViews() isExportingWatchViews = false if let path = watchExportPath { + #if DEBUG print("โŒš Watch views exported to: \(path.path)") + #endif openInFilesApp(path) } } @@ -596,7 +606,9 @@ struct SettingsContentView: View { insightsExportPath = await InsightsExporter.exportInsightsScreenshots() isExportingInsights = false if let path = insightsExportPath { + #if DEBUG print("โœจ Insights exported to: \(path.path)") + #endif openInFilesApp(path) } } @@ -656,7 +668,9 @@ struct SettingsContentView: View { sharingExportPath = await SharingScreenshotExporter.exportAllSharingScreenshots() isGeneratingScreenshots = false if let path = sharingExportPath { + #if DEBUG print("๐Ÿ“ธ Sharing screenshots exported to: \(path.path)") + #endif openInFilesApp(path) } } @@ -916,7 +930,9 @@ struct SettingsContentView: View { AnalyticsManager.shared.track(.healthKitNotAuthorized) } } catch { + #if DEBUG print("HealthKit authorization failed: \(error)") + #endif AnalyticsManager.shared.track(.healthKitEnableFailed) } } @@ -1014,7 +1030,7 @@ struct SettingsContentView: View { LocationManager.shared.requestAuthorization() // Check if permission was denied after a brief delay Task { - try? await Task.sleep(for: .seconds(1)) + try? await Task.sleep(for: .seconds(SettingsAnimationConstants.locationPermissionCheckDelay)) let status = LocationManager.shared.authorizationStatus if status == .denied || status == .restricted { weatherEnabled = false @@ -1445,9 +1461,13 @@ struct SettingsView: View { switch result { case .success(let url): AnalyticsManager.shared.track(.dataExported(format: "file", count: 0)) + #if DEBUG print("Saved to \(url)") + #endif case .failure(let error): + #if DEBUG print(error.localizedDescription) + #endif } }) .fileImporter(isPresented: $showingImporter, allowedContentTypes: [.text], @@ -1488,8 +1508,10 @@ struct SettingsView: View { } catch { // Handle failure. AnalyticsManager.shared.track(.importFailed(error: error.localizedDescription)) + #if DEBUG print("Unable to read file contents") print(error.localizedDescription) + #endif } } } @@ -1629,7 +1651,9 @@ struct SettingsView: View { AnalyticsManager.shared.track(.healthKitNotAuthorized) } } catch { + #if DEBUG print("HealthKit authorization failed: \(error)") + #endif AnalyticsManager.shared.track(.healthKitEnableFailed) } } @@ -1719,7 +1743,7 @@ struct SettingsView: View { LocationManager.shared.requestAuthorization() // Check if permission was denied after a brief delay Task { - try? await Task.sleep(for: .seconds(1)) + try? await Task.sleep(for: .seconds(SettingsAnimationConstants.locationPermissionCheckDelay)) let status = LocationManager.shared.authorizationStatus if status == .denied || status == .restricted { weatherEnabled = false @@ -2280,9 +2304,13 @@ struct SettingsView: View { let url = URL(fileURLWithPath: path) do { try image.jpegData(compressionQuality: 1.0)?.write(to: url, options: .atomic) + #if DEBUG print(url) + #endif } catch { + #if DEBUG print(error.localizedDescription) + #endif } } diff --git a/Shared/Views/SwitchableView.swift b/Shared/Views/SwitchableView.swift index fe404ab..7fdd468 100644 --- a/Shared/Views/SwitchableView.swift +++ b/Shared/Views/SwitchableView.swift @@ -101,6 +101,8 @@ struct SwitchableView: View { self.headerTypeChanged(viewType) AnalyticsManager.shared.track(.viewHeaderChanged(header: String(describing: viewType))) } + .accessibilityAddTraits(.isButton) + .accessibilityLabel(String(localized: "Switch header view")) } }