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:
@@ -112,9 +112,10 @@ class LiveActivityManager: ObservableObject {
|
||||
|
||||
// End activity at midnight
|
||||
func scheduleActivityEnd() {
|
||||
guard let activity = currentActivity else { return }
|
||||
guard let activity = currentActivity,
|
||||
let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: Date()) else { return }
|
||||
|
||||
let midnight = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: Date())!)
|
||||
let midnight = Calendar.current.startOfDay(for: tomorrow)
|
||||
|
||||
Task {
|
||||
await activity.end(nil, dismissalPolicy: .after(midnight))
|
||||
|
||||
@@ -31,21 +31,21 @@ class HealthService: ObservableObject {
|
||||
// MARK: - Data Types
|
||||
|
||||
// Core activity metrics
|
||||
private let stepCountType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
|
||||
private let exerciseTimeType = HKQuantityType.quantityType(forIdentifier: .appleExerciseTime)!
|
||||
private let activeEnergyType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!
|
||||
private let distanceType = HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)!
|
||||
private let stepCountType = HKQuantityType(.stepCount)
|
||||
private let exerciseTimeType = HKQuantityType(.appleExerciseTime)
|
||||
private let activeEnergyType = HKQuantityType(.activeEnergyBurned)
|
||||
private let distanceType = HKQuantityType(.distanceWalkingRunning)
|
||||
|
||||
// Heart & stress indicators
|
||||
private let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate)!
|
||||
private let restingHeartRateType = HKQuantityType.quantityType(forIdentifier: .restingHeartRate)!
|
||||
private let hrvType = HKQuantityType.quantityType(forIdentifier: .heartRateVariabilitySDNN)!
|
||||
private let heartRateType = HKQuantityType(.heartRate)
|
||||
private let restingHeartRateType = HKQuantityType(.restingHeartRate)
|
||||
private let hrvType = HKQuantityType(.heartRateVariabilitySDNN)
|
||||
|
||||
// Sleep & recovery
|
||||
private let sleepAnalysisType = HKCategoryType.categoryType(forIdentifier: .sleepAnalysis)!
|
||||
private let sleepAnalysisType = HKCategoryType(.sleepAnalysis)
|
||||
|
||||
// Mindfulness
|
||||
private let mindfulSessionType = HKCategoryType.categoryType(forIdentifier: .mindfulSession)!
|
||||
private let mindfulSessionType = HKCategoryType(.mindfulSession)
|
||||
|
||||
// State of Mind
|
||||
private let stateOfMindType = HKSampleType.stateOfMindType()
|
||||
@@ -117,7 +117,9 @@ class HealthService: ObservableObject {
|
||||
func fetchHealthData(for date: Date) async -> DailyHealthData {
|
||||
let calendar = Calendar.current
|
||||
let startOfDay = calendar.startOfDay(for: date)
|
||||
let endOfDay = calendar.date(byAdding: .day, value: 1, to: startOfDay)!
|
||||
guard let endOfDay = calendar.date(byAdding: .day, value: 1, to: startOfDay) else {
|
||||
return DailyHealthData(date: date, steps: nil, exerciseMinutes: nil, activeCalories: nil, distanceKm: nil, averageHeartRate: nil, restingHeartRate: nil, hrv: nil, sleepHours: nil, mindfulMinutes: nil)
|
||||
}
|
||||
|
||||
// Fetch all metrics concurrently
|
||||
async let steps = fetchSteps(start: startOfDay, end: endOfDay)
|
||||
@@ -233,8 +235,10 @@ class HealthService: ObservableObject {
|
||||
// Sleep data is typically recorded for the night before
|
||||
// So for mood on date X, we look at sleep from evening of X-1 to morning of X
|
||||
let calendar = Calendar.current
|
||||
let endOfSleep = calendar.date(bySettingHour: 12, minute: 0, second: 0, of: date)!
|
||||
let startOfSleep = calendar.date(byAdding: .hour, value: -18, to: endOfSleep)!
|
||||
guard let endOfSleep = calendar.date(bySettingHour: 12, minute: 0, second: 0, of: date),
|
||||
let startOfSleep = calendar.date(byAdding: .hour, value: -18, to: endOfSleep) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let predicate = HKQuery.predicateForSamples(withStart: startOfSleep, end: endOfSleep, options: .strictStartDate)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user