- 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>
102 lines
3.3 KiB
Swift
102 lines
3.3 KiB
Swift
//
|
|
// ContentModeViewModel.swift
|
|
// Feels (iOS)
|
|
//
|
|
// Created by Trey Tartt on 1/20/22.
|
|
//
|
|
|
|
import SwiftUI
|
|
import SwiftData
|
|
|
|
@MainActor
|
|
class DayViewViewModel: ObservableObject {
|
|
@Published var grouped = [Int: [Int: [MoodEntryModel]]]()
|
|
@Published var numberOfItems = 0
|
|
|
|
let addMonthStartWeekdayPadding: Bool
|
|
|
|
var hasNoData: Bool {
|
|
grouped.isEmpty
|
|
}
|
|
|
|
private var numberOfEntries: Int {
|
|
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) {
|
|
self.addMonthStartWeekdayPadding = addMonthStartWeekdayPadding
|
|
|
|
DataController.shared.addNewDataListener { [weak self] in
|
|
guard let self = self else { return }
|
|
// Avoid withAnimation for bulk data updates - it causes expensive view diffing
|
|
self.updateData()
|
|
}
|
|
updateData()
|
|
}
|
|
|
|
private func getGroupedData(addMonthStartWeekdayPadding: Bool) {
|
|
var newStuff = DataController.shared.splitIntoYearMonth(includedDays: [1,2,3,4,5,6,7])
|
|
if addMonthStartWeekdayPadding {
|
|
newStuff = MoodEntryFunctions.padMoodEntriesForCalendar(entries: newStuff)
|
|
}
|
|
grouped = newStuff
|
|
numberOfItems = numberOfEntries
|
|
}
|
|
|
|
public func updateData() {
|
|
getGroupedData(addMonthStartWeekdayPadding: self.addMonthStartWeekdayPadding)
|
|
}
|
|
|
|
public func add(mood: Mood, forDate date: Date, entryType: EntryType) {
|
|
MoodLogger.shared.logMood(mood, for: date, entryType: entryType)
|
|
}
|
|
|
|
public func update(entry: MoodEntryModel, toMood mood: Mood) {
|
|
if !MoodLogger.shared.updateMood(entryDate: entry.forDate, withMood: mood) {
|
|
print("Failed to update mood entry")
|
|
}
|
|
}
|
|
|
|
public func delete(offsets: IndexSet, inMonth month: Int, inYear year: Int) {
|
|
if let monthEntries = grouped[year],
|
|
let entries = monthEntries[month] {
|
|
var mutableEntries = entries.sorted(by: {
|
|
$0.forDate > $1.forDate
|
|
})
|
|
var entriesToDelete = [MoodEntryModel]()
|
|
for idx in offsets {
|
|
let obj = mutableEntries.remove(at: idx)
|
|
entriesToDelete.append(obj)
|
|
}
|
|
entriesToDelete.forEach({ entry in
|
|
MoodLogger.shared.deleteMood(forDate: entry.forDate)
|
|
})
|
|
}
|
|
}
|
|
|
|
static func updateTitleHeader(forEntry entry: MoodEntryModel?) -> String {
|
|
guard let entry = entry else {
|
|
return ""
|
|
}
|
|
|
|
let forDate = entry.forDate
|
|
|
|
let components = Calendar.current.dateComponents([.day, .month, .year], from: forDate)
|
|
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)
|
|
let dayz = Random.dayFormat(fromDate: entry.forDate)
|
|
|
|
let string = weekday + " " + monthName + " " + dayz + " " + String(year)
|
|
|
|
return String(format: String(localized: "content_view_fill_in_missing_entry"), string)
|
|
}
|
|
}
|