Fix 8 audit items: remove force-unwraps, improve accessibility and concurrency

- Replace force-unwrap HK types with modern HKQuantityType(_:) initializer
- Replace Calendar.date force-unwraps with guard/let in HealthService, HeaderPercView, MoodStreakActivity, DayViewViewModel, MonthTotalTemplate
- Extract DayViewViewModel.countEntries into testable static method with safe flatMap
- Replace DispatchQueue.main.asyncAfter with Task.sleep in CelebrationAnimations
- Add .minimumScaleFactor(0.5) to SmallRollUpHeaderView for Dynamic Type
- Add VoiceOver accessibility labels to HeaderPercView mood percentages
- Fix @testable import iFeel → Feels in Tests_iOS.swift
- Add 4 unit tests for countEntries (TDD)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-17 11:42:16 -06:00
parent 277e277750
commit 7f27446b94
11 changed files with 168 additions and 76 deletions

View File

@@ -106,7 +106,8 @@ struct CelebrationOverlayView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onAppear {
hasAppeared = true
DispatchQueue.main.asyncAfter(deadline: .now() + animationType.duration) {
Task { @MainActor in
try? await Task.sleep(for: .seconds(animationType.duration))
onComplete()
}
}
@@ -341,7 +342,8 @@ struct ShatterReformAnimation: View {
}
// Phase 2: Converge to center and fade
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
Task { @MainActor in
try? await Task.sleep(for: .seconds(0.6))
phase = .reform
withAnimation(.easeInOut(duration: 0.5)) {
for i in 0..<shardCount {
@@ -353,14 +355,16 @@ struct ShatterReformAnimation: View {
}
// Phase 3: Show checkmark
DispatchQueue.main.asyncAfter(deadline: .now() + 1.1) {
Task { @MainActor in
try? await Task.sleep(for: .seconds(1.1))
withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
checkmarkOpacity = 1
}
}
// Phase 4: Fade out
DispatchQueue.main.asyncAfter(deadline: .now() + 1.8) {
Task { @MainActor in
try? await Task.sleep(for: .seconds(1.8))
withAnimation(.easeOut(duration: 0.3)) {
checkmarkOpacity = 0
}

View File

@@ -20,15 +20,12 @@ class DayViewViewModel: ObservableObject {
}
private var numberOfEntries: Int {
var num = 0
grouped.keys.forEach({
let year = grouped[$0]
let monthKeys = year?.keys
monthKeys?.forEach({
num += year![$0]!.count
})
})
return num
Self.countEntries(in: grouped)
}
/// Count total entries across all year/month groups. Extracted for testability.
static func countEntries(in grouped: [Int: [Int: [MoodEntryModel]]]) -> Int {
grouped.values.flatMap(\.values).reduce(0) { $0 + $1.count }
}
init(addMonthStartWeekdayPadding: Bool) {
@@ -90,8 +87,8 @@ class DayViewViewModel: ObservableObject {
let forDate = entry.forDate
let components = Calendar.current.dateComponents([.day, .month, .year], from: forDate)
let month = components.month!
let year = components.year!
let month = components.month ?? 1
let year = components.year ?? Calendar.current.component(.year, from: Date())
let monthName = Random.monthName(fromMonthInt: month)
let weekday = Random.weekdayName(fromDate: entry.forDate)

View File

@@ -32,10 +32,10 @@ struct HeaderPercView: View {
if fakeData {
moodEntries = DataController.shared.randomEntries(count: 10)
} else {
var daysAgo = Calendar.current.date(byAdding: .day, value: -backDays, to: Date())!
daysAgo = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: daysAgo)!
moodEntries = DataController.shared.getData(startDate: daysAgo, endDate: Date(), includedDays: [1,2,3,4,5,6,7])
if let daysAgoRaw = Calendar.current.date(byAdding: .day, value: -backDays, to: Date()),
let daysAgo = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: daysAgoRaw) {
moodEntries = DataController.shared.getData(startDate: daysAgo, endDate: Date(), includedDays: [1,2,3,4,5,6,7])
}
}
if let moodEntries = moodEntries {
@@ -57,6 +57,7 @@ struct HeaderPercView: View {
.fontWeight(.bold)
.foregroundColor(moodTint.color(forMood: model.mood))
.frame(maxWidth: .infinity)
.accessibilityLabel("\(model.mood.strValue) \(Int(model.percent)) percent")
}
}
Spacer()
@@ -67,6 +68,7 @@ struct HeaderPercView: View {
.fontWeight(.bold)
.foregroundColor(moodTint.color(forMood: model.mood))
.frame(maxWidth: .infinity)
.accessibilityLabel("\(model.mood.strValue) \(Int(model.percent)) percent")
}
}
Spacer()
@@ -82,6 +84,7 @@ struct HeaderPercView: View {
bgColor: moodTint.color(forMood: model.mood),
textColor: textColor)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.accessibilityLabel("\(model.mood.strValue) \(Int(model.percent)) percent")
}
}
Spacer()
@@ -91,6 +94,7 @@ struct HeaderPercView: View {
bgColor: moodTint.color(forMood: model.mood),
textColor: textColor)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.accessibilityLabel("\(model.mood.strValue) \(Int(model.percent)) percent")
}
}
Spacer()

View File

@@ -17,7 +17,7 @@ struct MonthTotalTemplate: View, SharingTemplate {
var endDate: Date
var totalEntryCount: Int = 0
private var month = Calendar.current.dateComponents([.month], from: Date()).month!
private var month = Calendar.current.dateComponents([.month], from: Date()).month ?? 1
@State var showSharingTemplate = false
@StateObject private var shareImage = ShareImageStateViewModel()

View File

@@ -43,6 +43,7 @@ struct SmallRollUpHeaderView: View {
.font(.title2)
.fontWeight(.bold)
.lineLimit(1)
.minimumScaleFactor(0.5)
.foregroundColor(moodTint.color(forMood: model.mood))
.frame(maxWidth: .infinity, alignment: .center)
}