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:
Trey T
2026-03-26 20:09:14 -05:00
parent 4d9e906c4d
commit 1f040ab676
41 changed files with 427 additions and 107 deletions

View File

@@ -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!")

View File

@@ -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)
}

View File

@@ -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)")
}
}
}

View File

@@ -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)