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>
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user