diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 1fee4a3..cc6b530 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,7 +2,8 @@ "permissions": { "allow": [ "Bash(find:*)", - "Bash(xcodebuild:*)" + "Bash(xcodebuild:*)", + "Bash(cat:*)" ] } } diff --git a/Feels.xcodeproj/project.pbxproj b/Feels.xcodeproj/project.pbxproj index 00413ee..71ac81e 100644 --- a/Feels.xcodeproj/project.pbxproj +++ b/Feels.xcodeproj/project.pbxproj @@ -92,10 +92,9 @@ "Color+Codable.swift", "Date+Extensions.swift", EventLogger.swift, - Feels.xcdatamodeld, Models/DiamondView.swift, Models/Mood.swift, - Models/MoodEntryExtension.swift, + Models/MoodEntryModel.swift, Models/MoodImagable.swift, Models/MoodMetrics.swift, Models/MoodTintable.swift, @@ -105,11 +104,12 @@ Models/UserDefaultsStore.swift, Onboarding/OnboardingData.swift, Onboarding/views/OnboardingDay.swift, - Persisence/Persistence.swift, - Persisence/PersistenceADD.swift, - Persisence/PersistenceDELETE.swift, - Persisence/PersistenceGET.swift, - Persisence/PersistenceHelper.swift, + Persisence/DataController.swift, + Persisence/DataControllerADD.swift, + Persisence/DataControllerDELETE.swift, + Persisence/DataControllerGET.swift, + Persisence/DataControllerHelper.swift, + Persisence/SharedModelContainer.swift, Random.swift, ShowBasedOnVoteLogics.swift, Views/BGView.swift, diff --git a/FeelsWidget2/FeelsVoteWidget.swift b/FeelsWidget2/FeelsVoteWidget.swift index d183cc8..2376b31 100644 --- a/FeelsWidget2/FeelsVoteWidget.swift +++ b/FeelsWidget2/FeelsVoteWidget.swift @@ -33,7 +33,7 @@ struct VoteMoodIntent: AppIntent { let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding()) // Add mood entry - PersistenceController.shared.add(mood: mood, forDate: votingDate, entryType: .widget) + DataController.shared.add(mood: mood, forDate: votingDate, entryType: .widget) // Store last voted date let dateString = ISO8601DateFormatter().string(from: Calendar.current.startOfDay(for: votingDate)) @@ -63,19 +63,24 @@ struct VoteWidgetProvider: TimelineProvider { completion(entry) return } - let entry = createEntry() - completion(entry) + Task { @MainActor in + let entry = createEntry() + completion(entry) + } } func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { - let entry = createEntry() + Task { @MainActor in + let entry = createEntry() - // Refresh at midnight - let midnight = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: Date())!) - let timeline = Timeline(entries: [entry], policy: .after(midnight)) - completion(timeline) + // Refresh at midnight + let midnight = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: Date())!) + let timeline = Timeline(entries: [entry], policy: .after(midnight)) + completion(timeline) + } } + @MainActor private func createEntry() -> VoteWidgetEntry { let hasSubscription = GroupUserDefaults.groupDefaults.bool(forKey: UserDefaultsStore.Keys.hasActiveSubscription.rawValue) @@ -84,7 +89,7 @@ struct VoteWidgetProvider: TimelineProvider { let dayEnd = Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: dayStart)! // Check if user has voted today - let todayEntry = PersistenceController.shared.getData(startDate: dayStart, endDate: dayEnd, includedDays: []).first + let todayEntry = DataController.shared.getData(startDate: dayStart, endDate: dayEnd, includedDays: []).first let hasVotedToday = todayEntry != nil && todayEntry?.mood != Mood.missing && todayEntry?.mood != Mood.placeholder // Get today's mood if voted @@ -93,7 +98,7 @@ struct VoteWidgetProvider: TimelineProvider { // Get stats for display after voting var stats: MoodStats? = nil if hasVotedToday { - let allEntries = PersistenceController.shared.getData( + let allEntries = DataController.shared.getData( startDate: Date(timeIntervalSince1970: 0), endDate: Date(), includedDays: [] @@ -188,9 +193,10 @@ struct VotingView: View { } } else { // Horizontal layout for medium/large - HStack(spacing: 12) { + HStack(spacing: 4) { ForEach(moods, id: \.rawValue) { mood in MoodButton(mood: mood, isCompact: false) + .frame(maxWidth: .infinity) } } } @@ -224,6 +230,8 @@ struct MoodButton: View { Text(mood.widgetDisplayName) .font(.caption2) .foregroundStyle(.secondary) + .lineLimit(1) + .minimumScaleFactor(0.7) } } } diff --git a/FeelsWidget2/FeelsWidget.swift b/FeelsWidget2/FeelsWidget.swift index 302118d..3576f57 100644 --- a/FeelsWidget2/FeelsWidget.swift +++ b/FeelsWidget2/FeelsWidget.swift @@ -8,7 +8,7 @@ import WidgetKit import SwiftUI import Intents -import CoreData +import SwiftData class WatchTimelineView: Identifiable { let id = UUID() @@ -28,7 +28,7 @@ class WatchTimelineView: Identifiable { } struct TimeLineCreator { - static func createViews(daysBack: Int) -> [WatchTimelineView] { + @MainActor static func createViews(daysBack: Int) -> [WatchTimelineView] { var timeLineView = [WatchTimelineView]() let latestDayToShow = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding()) @@ -42,7 +42,7 @@ struct TimeLineCreator { let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable() let moodImages: MoodImagable.Type = UserDefaultsStore.moodMoodImagable() - if let todayEntry = PersistenceController.shared.getData(startDate: dayStart, endDate: dayEnd, includedDays: []).first { + if let todayEntry = DataController.shared.getData(startDate: dayStart, endDate: dayEnd, includedDays: []).first { timeLineView.append(WatchTimelineView(image: moodImages.icon(forMood: todayEntry.mood), graphic: moodImages.icon(forMood: todayEntry.mood), date: dayStart, @@ -99,7 +99,7 @@ struct Provider: IntentTimelineProvider { timeLineViews: TimeLineCreator.createSampleViews(count: 10)) } - func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) { + @MainActor func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) { // Use sample data for widget picker preview, real data otherwise let timeLineViews: [WatchTimelineView] if context.isPreview { @@ -165,9 +165,6 @@ struct FeelsWidgetEntryView : View { @unknown default: fatalError() } - }.onReceive(NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange)) { _ in - // make sure you don't call this too often - WidgetCenter.shared.reloadAllTimelines() } } } @@ -294,10 +291,6 @@ struct FeelsGraphicWidgetEntryView : View { @ViewBuilder var body: some View { SmallGraphicWidgetView(entry: entry) - .onReceive(NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange)) { _ in - // make sure you don't call this too often - WidgetCenter.shared.reloadAllTimelines() - } } } diff --git a/Shared/AppDelegate.swift b/Shared/AppDelegate.swift index 9660dba..de077e0 100644 --- a/Shared/AppDelegate.swift +++ b/Shared/AppDelegate.swift @@ -10,67 +10,63 @@ import UserNotifications import UIKit import WidgetKit import SwiftUI -// import Firebase // Firebase removed class AppDelegate: NSObject, UIApplicationDelegate { private let savedOnboardingData = UserDefaultsStore.getOnboarding() @AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor - + + @MainActor func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { - // PersistenceController.shared.clearDB() - // PersistenceController.shared.deleteLast(numberOfEntries: 5) -// PersistenceController.shared.deleteRandomFromLast(numberOfEntries: 10) - // GroupUserDefaults.groupDefaults.set(false, forKey: UserDefaultsStore.Keys.showNSFW.rawValue) - - // FirebaseApp.configure() // Firebase removed - PersistenceController.shared.removeNoForDates() - PersistenceController.shared.fillInMissingDates() + DataController.shared.removeNoForDates() + DataController.shared.fillInMissingDates() UNUserNotificationCenter.current().delegate = self - + UIPageControl.appearance().currentPageIndicatorTintColor = UIColor(textColor) UIPageControl.appearance().pageIndicatorTintColor = UIColor.systemGray - + let appearance = UITabBarAppearance() appearance.configureWithOpaqueBackground() UITabBar.appearance().standardAppearance = appearance UITabBar.appearance().scrollEdgeAppearance = appearance - + EventLogger.log(event: "app_launced") - + return true } - + + @MainActor func applicationWillEnterForeground(_ application: UIApplication) { - PersistenceController.shared.fillInMissingDates() - + DataController.shared.fillInMissingDates() + // reschedule notifications so there's a new title next notification LocalNotification.rescheduleNotifiations() - + EventLogger.log(event: "app_foregorund") } } extension AppDelegate: UNUserNotificationCenterDelegate { func requestAuthorization() { } - + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.badge, .banner, .sound]) } - + + @MainActor func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { if let action = LocalNotification.ActionType(rawValue: response.actionIdentifier) { let date = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: savedOnboardingData) switch action { case .horrible: - PersistenceController.shared.add(mood: .horrible, forDate: date, entryType: .notification) + DataController.shared.add(mood: .horrible, forDate: date, entryType: .notification) case .bad: - PersistenceController.shared.add(mood: .bad, forDate: date, entryType: .notification) + DataController.shared.add(mood: .bad, forDate: date, entryType: .notification) case .average: - PersistenceController.shared.add(mood: .average, forDate: date, entryType: .notification) + DataController.shared.add(mood: .average, forDate: date, entryType: .notification) case .good: - PersistenceController.shared.add(mood: .good, forDate: date, entryType: .notification) + DataController.shared.add(mood: .good, forDate: date, entryType: .notification) case .great: - PersistenceController.shared.add(mood: .great, forDate: date, entryType: .notification) + DataController.shared.add(mood: .great, forDate: date, entryType: .notification) } UIApplication.shared.applicationIconBadgeNumber = 0 } diff --git a/Shared/BGTask.swift b/Shared/BGTask.swift index e840e61..88a4a19 100644 --- a/Shared/BGTask.swift +++ b/Shared/BGTask.swift @@ -10,15 +10,16 @@ import BackgroundTasks class BGTask { static let updateDBMissingID = "com.tt.ifeel.dbUpdateMissing" - + + @MainActor class func runFillInMissingDatesTask(task: BGProcessingTask) { BGTask.scheduleBackgroundProcessing() - + task.expirationHandler = { task.setTaskCompleted(success: false) } - - PersistenceController.shared.fillInMissingDates() + + DataController.shared.fillInMissingDates() task.setTaskCompleted(success: true) } diff --git a/Shared/Feels.xcdatamodeld/.xccurrentversion b/Shared/Feels.xcdatamodeld/.xccurrentversion deleted file mode 100644 index bc6bac5..0000000 --- a/Shared/Feels.xcdatamodeld/.xccurrentversion +++ /dev/null @@ -1,8 +0,0 @@ - - - - - _XCCurrentVersionName - Shared 2.xcdatamodel - - diff --git a/Shared/Feels.xcdatamodeld/Shared 2.xcdatamodel/contents b/Shared/Feels.xcdatamodeld/Shared 2.xcdatamodel/contents deleted file mode 100644 index 8863425..0000000 --- a/Shared/Feels.xcdatamodeld/Shared 2.xcdatamodel/contents +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Shared/Feels.xcdatamodeld/Shared.xcdatamodel/contents b/Shared/Feels.xcdatamodeld/Shared.xcdatamodel/contents deleted file mode 100644 index 64afd9a..0000000 --- a/Shared/Feels.xcdatamodeld/Shared.xcdatamodel/contents +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Shared/FeelsApp.swift b/Shared/FeelsApp.swift index 6f2b3e9..c33c46f 100644 --- a/Shared/FeelsApp.swift +++ b/Shared/FeelsApp.swift @@ -6,6 +6,7 @@ // import SwiftUI +import SwiftData import BackgroundTasks import WidgetKit @@ -13,8 +14,8 @@ import WidgetKit struct FeelsApp: App { @Environment(\.scenePhase) private var scenePhase @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate - - let persistenceController = PersistenceController.shared + + let dataController = DataController.shared @StateObject var iapManager = IAPManager() @AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults) private var firstLaunchDate = Date() @State private var showSubscriptionFromWidget = false @@ -25,20 +26,16 @@ struct FeelsApp: App { BGTask.runFillInMissingDatesTask(task: task as! BGProcessingTask) } UIApplication.shared.applicationIconBadgeNumber = 0 -// PersistenceController.shared.clearDB() -// PersistenceController.shared.populateMemory() } - + var body: some Scene { WindowGroup { - // build these here so when tints and other things get updated the views / their data dont - // have to get redrawn#imageLiteral(resourceName: "simulator_screenshot_0017B4DC-100B-42A3-A406-9019704AE275.png") MainTabView(dayView: DayView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: false)), monthView: MonthView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: true)), yearView: YearView(viewModel: YearViewModel()), insightsView: InsightsView(), customizeView: CustomizeView()) - .environment(\.managedObjectContext, persistenceController.viewContext) + .modelContainer(dataController.container) .environmentObject(iapManager) .sheet(isPresented: $showSubscriptionFromWidget) { FeelsSubscriptionStoreView() diff --git a/Shared/Models/MoodEntryExtension.swift b/Shared/Models/MoodEntryExtension.swift deleted file mode 100644 index 584acfd..0000000 --- a/Shared/Models/MoodEntryExtension.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// MoodEntryExtension.swift -// Feels -// -// Created by Trey Tartt on 1/5/22. -// - -import Foundation - -enum EntryType: Int { - case notification - case header - case listView - case filledInMissing - case widget -} - -extension MoodEntry { - var moodString: String { - return Mood.init(rawValue: Int(self.moodValue))?.strValue ?? "NA" - } - - var mood: Mood { - return Mood.init(rawValue: Int(self.moodValue))! - } -} diff --git a/Shared/Models/MoodEntryModel.swift b/Shared/Models/MoodEntryModel.swift new file mode 100644 index 0000000..61673c4 --- /dev/null +++ b/Shared/Models/MoodEntryModel.swift @@ -0,0 +1,81 @@ +// +// MoodEntryModel.swift +// Feels +// +// SwiftData model replacing Core Data MoodEntry +// + +import Foundation +import SwiftData + +// MARK: - Entry Type Enum + +enum EntryType: Int, Codable { + case listView = 0 + case widget = 1 + case watch = 2 + case shortcut = 3 + case filledInMissing = 4 + case notification = 5 + case header = 6 +} + +// MARK: - SwiftData Model + +@Model +final class MoodEntryModel { + // Primary attributes + var forDate: Date + var moodValue: Int + var timestamp: Date + var weekDay: Int + var entryType: Int + + // Metadata + var canEdit: Bool + var canDelete: Bool + + // Computed properties + var mood: Mood { + Mood(rawValue: moodValue) ?? .missing + } + + var moodString: String { + mood.strValue + } + + init( + forDate: Date, + mood: Mood, + entryType: EntryType, + canEdit: Bool = true, + canDelete: Bool = true + ) { + self.forDate = forDate + self.moodValue = mood.rawValue + self.timestamp = Date() + self.weekDay = Calendar.current.component(.weekday, from: forDate) + self.entryType = entryType.rawValue + self.canEdit = canEdit + self.canDelete = canDelete + } + + // Convenience initializer for raw values + init( + forDate: Date, + moodValue: Int, + entryType: Int, + timestamp: Date = Date(), + weekDay: Int? = nil, + canEdit: Bool = true, + canDelete: Bool = true + ) { + self.forDate = forDate + self.moodValue = moodValue + self.timestamp = timestamp + self.weekDay = weekDay ?? Calendar.current.component(.weekday, from: forDate) + self.entryType = entryType + self.canEdit = canEdit + self.canDelete = canDelete + } +} diff --git a/Shared/MoodEntryFunctions.swift b/Shared/MoodEntryFunctions.swift index 814c09e..7da8668 100644 --- a/Shared/MoodEntryFunctions.swift +++ b/Shared/MoodEntryFunctions.swift @@ -7,13 +7,14 @@ import Foundation +@MainActor class MoodEntryFunctions { - static func padMoodEntriesForCalendar(entries grouped: [Int: [Int: [MoodEntry]]]) -> [Int: [Int: [MoodEntry]]] { - var newGrouped = [Int: [Int: [MoodEntry]]]() - + static func padMoodEntriesForCalendar(entries grouped: [Int: [Int: [MoodEntryModel]]]) -> [Int: [Int: [MoodEntryModel]]] { + var newGrouped = [Int: [Int: [MoodEntryModel]]]() + let allYears = grouped.keys.sorted(by: > ) for year in allYears { - var newMonth = [Int: [MoodEntry]]() + var newMonth = [Int: [MoodEntryModel]]() let oldMonths = grouped[year]! let monthKeys = oldMonths.keys.sorted(by: > ) @@ -26,38 +27,38 @@ class MoodEntryFunctions { } return newGrouped } - - static func padMoodEntriesMonth(monthEntries entries: [MoodEntry]) -> [MoodEntry] { - let sortedEntries = entries.sorted(by: { $0.forDate! < $1.forDate! }) + + static func padMoodEntriesMonth(monthEntries entries: [MoodEntryModel]) -> [MoodEntryModel] { + let sortedEntries = entries.sorted(by: { $0.forDate < $1.forDate }) var mutableEntries = sortedEntries - + if let firstDate = sortedEntries.first { - let date = firstDate.forDate! - + let date = firstDate.forDate + // if the first entry for a month is in the middle of the month we // need to add in the missing entries, as placeholders, to the beignning to get // the entries on the right day. think user downloads in the middle of the month // and entry is on the 13th ... this needs to show on the 13th entry spot var startOfMonth = date.startOfMonth startOfMonth = Calendar.current.date(byAdding: .hour, value: 9, to: startOfMonth)! - let lastMissingDate = mutableEntries.first?.forDate ?? date.endOfMonth + let lastMissingDate = mutableEntries.first?.forDate ?? date.endOfMonth var missingDates = Date.dates(from: startOfMonth, toDate: lastMissingDate, includingToDate: true) missingDates = missingDates.dropLast() - + for date in missingDates { - mutableEntries.insert(PersistenceController.shared.generateObjectNotInArray(forDate: date, withMood: .placeholder), at: 0) + mutableEntries.insert(DataController.shared.generateObjectNotInArray(forDate: date, withMood: .placeholder), at: 0) } - + mutableEntries = mutableEntries.sorted(by: { - $0.forDate! < $1.forDate! + $0.forDate < $1.forDate }) - + // fill in calendar day offset .. if month starts on wed we need to // pad the beginning sun, mon, tues if let firstDate = mutableEntries.first?.forDate { let weekday = Int16(Calendar.current.component(.weekday, from: firstDate)) for _ in 1.. Void)]() + private var editedDataClosure = [() -> Void]() + + // Computed properties for earliest/latest entries + var earliestEntry: MoodEntryModel? { + var descriptor = FetchDescriptor( + sortBy: [SortDescriptor(\.forDate, order: .forward)] + ) + descriptor.fetchLimit = 1 + return try? modelContext.fetch(descriptor).first + } + + var latestEntry: MoodEntryModel? { + var descriptor = FetchDescriptor( + sortBy: [SortDescriptor(\.forDate, order: .reverse)] + ) + descriptor.fetchLimit = 1 + return try? modelContext.fetch(descriptor).first + } + + private init() { + let cloudKit = GroupUserDefaults.groupDefaults.bool(forKey: UserDefaultsStore.Keys.useCloudKit.rawValue) + container = SharedModelContainer.create(useCloudKit: cloudKit) + } + + // MARK: - Container Switching (for CloudKit toggle) + + func switchContainer() { + save() + container = SharedModelContainer.create(useCloudKit: useCloudKit) + for listener in switchContainerListeners { + listener() + } + } + + // MARK: - Listener Management + + func addNewDataListener(closure: @escaping (() -> Void)) { + editedDataClosure.append(closure) + } + + func saveAndRunDataListeners() { + save() + for closure in editedDataClosure { + closure() + } + } + + func save() { + guard modelContext.hasChanges else { return } + do { + try modelContext.save() + } catch { + print("Failed to save context: \(error)") + } + } +} diff --git a/Shared/Persisence/DataControllerADD.swift b/Shared/Persisence/DataControllerADD.swift new file mode 100644 index 0000000..9c80411 --- /dev/null +++ b/Shared/Persisence/DataControllerADD.swift @@ -0,0 +1,77 @@ +// +// DataControllerADD.swift +// Feels +// +// SwiftData CREATE operations. +// + +import SwiftData +import Foundation + +extension DataController { + func add(mood: Mood, forDate date: Date, entryType: EntryType) { + // Delete existing entry for this date if present + if let existing = getEntry(byDate: date) { + modelContext.delete(existing) + try? modelContext.save() + } + + let entry = MoodEntryModel( + forDate: date, + mood: mood, + entryType: entryType + ) + + modelContext.insert(entry) + EventLogger.log(event: "add_entry", withData: ["entry_type": entryType.rawValue]) + saveAndRunDataListeners() + } + + func fillInMissingDates() { + let currentOnboarding = UserDefaultsStore.getOnboarding() + var endDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: currentOnboarding) + // Since it's for views, take away the last date so vote is enabled + endDate = Calendar.current.date(byAdding: .day, value: -1, to: endDate)! + + let descriptor = FetchDescriptor( + sortBy: [SortDescriptor(\.forDate, order: .reverse)] + ) + + guard let entries = try? modelContext.fetch(descriptor), + let firstEntry = entries.last else { return } + + let allDates: [Date] = Date.dates(from: firstEntry.forDate, toDate: endDate, includingToDate: true).map { + Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: $0)! + } + + let existingDates: Set = Set(entries.map { + Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: $0.forDate)! + }) + + let missing = Array(Set(allDates).subtracting(existingDates)).sorted(by: >) + + for date in missing { + // Add 12 hours to avoid UTC offset issues + let adjustedDate = Calendar.current.date(byAdding: .hour, value: 12, to: date)! + add(mood: .missing, forDate: adjustedDate, entryType: .filledInMissing) + } + + if !missing.isEmpty { + EventLogger.log(event: "filled_in_missing_entries", withData: ["count": missing.count]) + } + } + + func fixWrongWeekdays() { + let data = getData(startDate: Date(timeIntervalSince1970: 0), endDate: Date(), includedDays: []) + for entry in data { + entry.weekDay = Calendar.current.component(.weekday, from: entry.forDate) + } + save() + } + + func removeNoForDates() { + // Note: With SwiftData's non-optional forDate, this is essentially a no-op + // Keeping for API compatibility + EventLogger.log(event: "removed_entry_no_for_date", withData: ["count": 0]) + } +} diff --git a/Shared/Persisence/DataControllerDELETE.swift b/Shared/Persisence/DataControllerDELETE.swift new file mode 100644 index 0000000..2c21974 --- /dev/null +++ b/Shared/Persisence/DataControllerDELETE.swift @@ -0,0 +1,40 @@ +// +// DataControllerDELETE.swift +// Feels +// +// SwiftData DELETE operations. +// + +import SwiftData +import Foundation + +extension DataController { + func clearDB() { + do { + try modelContext.delete(model: MoodEntryModel.self) + saveAndRunDataListeners() + } catch { + fatalError("Failed to clear database: \(error)") + } + } + + func deleteLast(numberOfEntries: Int) { + let startDate = Calendar.current.date(byAdding: .day, value: -numberOfEntries, to: Date())! + let entries = getData(startDate: startDate, endDate: Date(), includedDays: []) + + for entry in entries { + modelContext.delete(entry) + } + save() + } + + func deleteRandomFromLast(numberOfEntries: Int) { + let startDate = Calendar.current.date(byAdding: .day, value: -numberOfEntries, to: Date())! + let entries = getData(startDate: startDate, endDate: Date(), includedDays: []) + + for entry in entries where Bool.random() { + modelContext.delete(entry) + } + save() + } +} diff --git a/Shared/Persisence/DataControllerGET.swift b/Shared/Persisence/DataControllerGET.swift new file mode 100644 index 0000000..a0d2777 --- /dev/null +++ b/Shared/Persisence/DataControllerGET.swift @@ -0,0 +1,85 @@ +// +// DataControllerGET.swift +// Feels +// +// SwiftData READ operations. +// + +import SwiftData +import Foundation + +extension DataController { + func getEntry(byDate date: Date) -> MoodEntryModel? { + let startDate = Calendar.current.startOfDay(for: date) + let endDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate)! + + var descriptor = FetchDescriptor( + predicate: #Predicate { entry in + entry.forDate >= startDate && entry.forDate <= endDate + }, + sortBy: [SortDescriptor(\.forDate, order: .forward)] + ) + descriptor.fetchLimit = 1 + + return try? modelContext.fetch(descriptor).first + } + + func getData(startDate: Date, endDate: Date, includedDays: [Int]) -> [MoodEntryModel] { + let weekDays = includedDays.isEmpty ? [1, 2, 3, 4, 5, 6, 7] : includedDays + + let descriptor = FetchDescriptor( + predicate: #Predicate { entry in + entry.forDate >= startDate && + entry.forDate <= endDate && + weekDays.contains(entry.weekDay) + }, + sortBy: [SortDescriptor(\.forDate, order: .forward)] + ) + + return (try? modelContext.fetch(descriptor)) ?? [] + } + + func splitIntoYearMonth(includedDays: [Int]) -> [Int: [Int: [MoodEntryModel]]] { + let data = getData( + startDate: Date(timeIntervalSince1970: 0), + endDate: Date(), + includedDays: includedDays + ).sorted { $0.forDate < $1.forDate } + + guard let earliest = data.first, + let latest = data.last else { return [:] } + + let calendar = Calendar.current + let earliestYear = calendar.component(.year, from: earliest.forDate) + let latestYear = calendar.component(.year, from: latest.forDate) + + var result = [Int: [Int: [MoodEntryModel]]]() + + for year in earliestYear...latestYear { + var monthData = [Int: [MoodEntryModel]]() + + for month in 1...12 { + var components = DateComponents() + components.year = year + components.month = month + components.day = 1 + + guard let startOfMonth = calendar.date(from: components) else { continue } + + let items = getData( + startDate: startOfMonth, + endDate: startOfMonth.endOfMonth, + includedDays: [1, 2, 3, 4, 5, 6, 7] + ) + + if !items.isEmpty { + monthData[month] = items + } + } + + result[year] = monthData + } + + return result + } +} diff --git a/Shared/Persisence/DataControllerHelper.swift b/Shared/Persisence/DataControllerHelper.swift new file mode 100644 index 0000000..af95d19 --- /dev/null +++ b/Shared/Persisence/DataControllerHelper.swift @@ -0,0 +1,93 @@ +// +// DataControllerHelper.swift +// Feels +// +// SwiftData helper and test data operations. +// + +import SwiftData +import Foundation + +extension DataController { + func randomEntries(count: Int) -> [MoodEntryModel] { + var entries = [MoodEntryModel]() + + for idx in 0.. MoodEntryModel { + var moodValue = Int.random(in: 2...4) + if Int.random(in: 0...400) % 5 == 0 { + moodValue = Int.random(in: 0...4) + } + + let entry = MoodEntryModel( + forDate: date, + moodValue: moodValue, + entryType: EntryType.listView.rawValue, + canEdit: false, + canDelete: false + ) + return entry + } + + func populateTestData() { + clearDB() + + for idx in 1..<1000 { + let date = Calendar.current.date(byAdding: .day, value: -idx, to: Date())! + var moodValue = Int.random(in: 3...4) + if Int.random(in: 0...400) % 5 == 0 { + moodValue = Int.random(in: 0...4) + } + + let entry = MoodEntryModel( + forDate: date, + mood: Mood(rawValue: moodValue) ?? .average, + entryType: .listView + ) + modelContext.insert(entry) + } + + saveAndRunDataListeners() + } + + func longestStreak() -> [MoodEntryModel] { + let descriptor = FetchDescriptor( + sortBy: [SortDescriptor(\.forDate, order: .forward)] + ) + return (try? modelContext.fetch(descriptor)) ?? [] + } +} diff --git a/Shared/Persisence/DataControllerUPDATE.swift b/Shared/Persisence/DataControllerUPDATE.swift new file mode 100644 index 0000000..f6443a2 --- /dev/null +++ b/Shared/Persisence/DataControllerUPDATE.swift @@ -0,0 +1,24 @@ +// +// DataControllerUPDATE.swift +// Feels +// +// SwiftData UPDATE operations. +// + +import SwiftData +import Foundation + +extension DataController { + @discardableResult + func update(entryDate: Date, withMood mood: Mood) -> Bool { + guard let entry = getEntry(byDate: entryDate) else { + return false + } + + entry.moodValue = mood.rawValue + saveAndRunDataListeners() + + EventLogger.log(event: "update_entry") + return true + } +} diff --git a/Shared/Persisence/Persistence.swift b/Shared/Persisence/Persistence.swift deleted file mode 100644 index 32fe751..0000000 --- a/Shared/Persisence/Persistence.swift +++ /dev/null @@ -1,137 +0,0 @@ -// -// Persistence.swift -// Shared -// -// Created by Trey Tartt on 1/5/22. -// - -import CoreData -import SwiftUI - -class PersistenceController { - @AppStorage(UserDefaultsStore.Keys.useCloudKit.rawValue, store: GroupUserDefaults.groupDefaults) - - private var useCloudKit = false - - static let shared = PersistenceController.persistenceController - - private static var persistenceController: PersistenceController { - return PersistenceController(inMemory: true) - } - - public var viewContext: NSManagedObjectContext { - return PersistenceController.shared.container.viewContext - } - - public lazy var childContext: NSManagedObjectContext = { - NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) - }() - - public var switchContainerListeners = [(() -> Void)]() - - private var editedDataClosure = [() -> Void]() - - public var earliestEntry: MoodEntry? { - let fetchRequest = NSFetchRequest(entityName: "MoodEntry") - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)] - let first = try! viewContext.fetch(fetchRequest).first - return first ?? nil - } - - public var latestEntry: MoodEntry? { - let fetchRequest = NSFetchRequest(entityName: "MoodEntry") - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)] - let last = try! viewContext.fetch(fetchRequest).last - return last ?? nil - } - - lazy var container: NSPersistentContainer = { - setupContainer() - }() - - func switchContainer() { - try? viewContext.save() - container = setupContainer() - for item in switchContainerListeners { - item() - } - } - - public func addNewDataListener(closure: @escaping (() -> Void)) { - editedDataClosure.append(closure) - } - - public func saveAndRunDataListerners() { - do { - try viewContext.save() - - for closure in editedDataClosure { - closure() - } - } catch { - print(error) - } - } - - private func setupContainer() -> NSPersistentContainer { - if useCloudKit { - container = NSPersistentCloudKitContainer(name: "Feels") - } else { - container = NSCustomPersistentContainer(name: "Feels") - } - - for description in container.persistentStoreDescriptions { - description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey) - description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) - description.setOption(true as NSNumber, forKey: NSMigratePersistentStoresAutomaticallyOption) - description.setOption(true as NSNumber, forKey: NSInferMappingModelAutomaticallyOption) - } - - container.loadPersistentStores(completionHandler: { (storeDescription, error) in - self.container.viewContext.automaticallyMergesChangesFromParent = true - - if let error = error as NSError? { - fatalError("Unresolved error \(error), \(error.userInfo)") - } - }) - - return container - } - - init(inMemory: Bool = false) { - container = setupContainer() - } -} - -extension NSManagedObjectContext { - /// Executes the given `NSBatchDeleteRequest` and directly merges the changes to bring the given managed object context up to date. - /// - /// - Parameter batchDeleteRequest: The `NSBatchDeleteRequest` to execute. - /// - Throws: An error if anything went wrong executing the batch deletion. - public func executeAndMergeChanges(using batchDeleteRequest: NSBatchDeleteRequest) throws { - batchDeleteRequest.resultType = .resultTypeObjectIDs - let result = try execute(batchDeleteRequest) as? NSBatchDeleteResult - let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []] - NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self]) - } -} - -class NSCustomPersistentContainer: NSPersistentContainer { - override open class func defaultDirectoryURL() -> URL { -#if DEBUG - if let storeURLDebug = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.groupShareIdDebug) { - return storeURLDebug.appendingPathComponent("Feels-Debug.sqlite") - } - // Fallback to default location if App Group not available - print("⚠️ App Group not available, using default Core Data location") - return super.defaultDirectoryURL() -#else - if let storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.groupShareId) { - return storeURL.appendingPathComponent("Feels.sqlite") - } - // Fallback to default location if App Group not available - print("⚠️ App Group not available, using default Core Data location") - return super.defaultDirectoryURL() -#endif - } -} diff --git a/Shared/Persisence/PersistenceADD.swift b/Shared/Persisence/PersistenceADD.swift deleted file mode 100644 index ea7e0be..0000000 --- a/Shared/Persisence/PersistenceADD.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// PersistenceADD.swift -// Feels -// -// Created by Trey Tartt on 2/17/22. -// - -import CoreData - -extension PersistenceController { - public func fixWrongWeekdays() { - let data = PersistenceController.shared.getData(startDate: Date(timeIntervalSince1970: 0), - endDate: Date(), - includedDays: []).sorted(by: { - $0.forDate! < $1.forDate! - }) - - data.forEach({ - $0.weekDay = Int16(Calendar.current.component(.weekday, from: $0.forDate!)) - }) - try? viewContext.save() - } - - public func add(mood: Mood, forDate date: Date, entryType: EntryType) { - if let existingEntry = getEntry(byDate: date) { - viewContext.delete(existingEntry) - try? viewContext.save() - } - - let newItem = MoodEntry(context: viewContext) - newItem.timestamp = Date() - newItem.moodValue = Int16(mood.rawValue) - newItem.forDate = date - newItem.weekDay = Int16(Calendar.current.component(.weekday, from: date)) - newItem.canEdit = true - newItem.canDelete = true - newItem.entryType = Int16(entryType.rawValue) - - EventLogger.log(event: "add_entry", withData: ["entry_type": entryType.rawValue]) - - saveAndRunDataListerners() - } - - func fillInMissingDates() { - let currentOnboarding = UserDefaultsStore.getOnboarding() - var endDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: currentOnboarding) - // since its for views take away the last date so vote is enabled - endDate = Calendar.current.date(byAdding: .day, value: -1, to: endDate)! - - let fetchRequest = NSFetchRequest(entityName: "MoodEntry") - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: false)] - let entries = try! viewContext.fetch(fetchRequest) - - if let firstEntry = entries.last?.forDate { - let allDates: [Date] = Date.dates(from: firstEntry, toDate: endDate, includingToDate: true).map({ - let zeroDate = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: $0)! - return zeroDate - }) - - let existingEntries: [Date] = entries.compactMap({ - if let date = $0.forDate { - let zeroDate = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: date)! - return zeroDate - } - return nil - }) - - let allDatesSet = Set(allDates) - let existingEntriesSet = Set(existingEntries) - let missing = Array(allDatesSet.subtracting(existingEntriesSet)).sorted(by: >) - for date in missing { - // add 12 hours, if you enter a things right at 12:00.00 it wont show .... mabye - // due to utc offset? - let adjustedDate = Calendar.current.date(byAdding: .hour, value: 12, to: date)! - add(mood: .missing, forDate: adjustedDate, entryType: .filledInMissing) - } - - if !missing.isEmpty { - EventLogger.log(event: "filled_in_missing_entries", withData: ["count": missing.count]) - } - } - } - - func removeNoForDates() { - let fetchRequest = NSFetchRequest(entityName: "MoodEntry") - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: false)] - let entries = try! viewContext.fetch(fetchRequest) - - for entry in entries { - guard let _ = entry.forDate else { - viewContext.delete(entry) - try? viewContext.save() - return - } - } - EventLogger.log(event: "removed_entry_no_for_date", withData: ["count": entries.count]) - } -} diff --git a/Shared/Persisence/PersistenceDELETE.swift b/Shared/Persisence/PersistenceDELETE.swift deleted file mode 100644 index dbc5172..0000000 --- a/Shared/Persisence/PersistenceDELETE.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// PersistenceDELETE.swift -// Feels -// -// Created by Trey Tartt on 2/17/22. -// - -import CoreData - -extension PersistenceController { - func clearDB() { - let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "MoodEntry") - let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) - - do { - try viewContext.executeAndMergeChanges(using: deleteRequest) - saveAndRunDataListerners() - } catch let error as NSError { - fatalError("Unresolved error \(error), \(error.userInfo)") - } - } - - func deleteLast(numberOfEntries: Int) { - let entries = PersistenceController.shared.getData(startDate: Calendar.current.date(byAdding: .day, value: -numberOfEntries, to: Date())!, - endDate: Date(), - includedDays: []) - for entry in entries { - viewContext.delete(entry) - } - try! viewContext.save() - } - - func deleteRandomFromLast(numberOfEntries: Int) { - let entries = PersistenceController.shared.getData(startDate: Calendar.current.date(byAdding: .day, value: -numberOfEntries, to: Date())!, - endDate: Date(), - includedDays: []) - for entry in entries { - if Bool.random() { - viewContext.delete(entry) - } - } - try! viewContext.save() - } -} diff --git a/Shared/Persisence/PersistenceGET.swift b/Shared/Persisence/PersistenceGET.swift deleted file mode 100644 index 57cfde2..0000000 --- a/Shared/Persisence/PersistenceGET.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// PersistenceGET.swift -// Feels -// -// Created by Trey Tartt on 2/17/22. -// - -import CoreData - -extension PersistenceController { - public func getEntry(byDate date: Date) -> MoodEntry? { - let startDate = Calendar.current.startOfDay(for: date) - let endDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate)! - - let predicate = NSPredicate(format: "forDate >= %@ && forDate <= %@ ", - startDate as NSDate, - endDate as NSDate) - - let fetchRequest = NSFetchRequest(entityName: "MoodEntry") - fetchRequest.predicate = predicate - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)] - let data = try! viewContext.fetch(fetchRequest) - return data.first - } - - public func getData(startDate: Date, endDate: Date, includedDays: [Int]) -> [MoodEntry] { - try! viewContext.setQueryGenerationFrom(.current) -// viewContext.refreshAllObjects() - - var includedDays16 = [Int16]() - - if includedDays.isEmpty { - includedDays16 = [Int16(1), Int16(2), Int16(3), Int16(4), Int16(5), Int16(6), Int16(7)] - } else { - includedDays16 = includedDays.map({ - Int16($0) - }) - } - let predicate = NSPredicate(format: "forDate >= %@ && forDate <= %@ && weekDay IN %@", - startDate as NSDate, - endDate as NSDate, - includedDays16) - - let fetchRequest = NSFetchRequest(entityName: "MoodEntry") - fetchRequest.predicate = predicate - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)] - let data = try! viewContext.fetch(fetchRequest) - return data - } - - public func splitIntoYearMonth(includedDays: [Int]) -> [Int: [Int: [MoodEntry]]] { - let data = PersistenceController.shared.getData(startDate: Date(timeIntervalSince1970: 0), - endDate: Date(), - includedDays: includedDays).sorted(by: { - $0.forDate! < $1.forDate! - }) - var returnData = [Int: [Int: [MoodEntry]]]() - - if let earliestEntry = data.first, - let lastEntry = data.last { - - let calendar = Calendar.current - let components = calendar.dateComponents([.year], from: earliestEntry.forDate!) - let earliestYear = components.year! - - let latestComponents = calendar.dateComponents([.year], from: lastEntry.forDate!) - let latestYear = latestComponents.year! - - for year in earliestYear...latestYear { - var allMonths = [Int: [MoodEntry]]() - - for month in (1...12) { - var components = DateComponents() - components.month = month - components.year = year - components.day = 1 - let startDateOfMonth = Calendar.current.date(from: components)! - - let items = PersistenceController.shared.getData(startDate: startDateOfMonth, - endDate: startDateOfMonth.endOfMonth, - includedDays: [1,2,3,4,5,6,7]) - - if !items.isEmpty { - allMonths[month] = items - } - } - returnData[year] = allMonths - } - } - return returnData - } -} diff --git a/Shared/Persisence/PersistenceHelper.swift b/Shared/Persisence/PersistenceHelper.swift deleted file mode 100644 index b55132c..0000000 --- a/Shared/Persisence/PersistenceHelper.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// PersistenceHelper.swift -// Feels (iOS) -// -// Created by Trey Tartt on 2/17/22. -// - -import CoreData - -extension PersistenceController { - public func randomEntries(count: Int) -> [MoodEntry] { - var entries = [MoodEntry]() - - for idx in 0.. MoodEntry { - let newItem = MoodEntry(context: childContext) - newItem.timestamp = Date() - newItem.moodValue = Int16.random(in: 2 ... 4) - if Int16.random(in: 0 ... 400) % 5 == 0 { - newItem.moodValue = Int16.random(in: 0 ... 4) - } - newItem.forDate = date - newItem.weekDay = Int16(Calendar.current.component(.weekday, from: Date())) - newItem.canEdit = false - newItem.canDelete = false - return newItem - } - - func populateTestData() { - do { - self.clearDB() - try viewContext.save() - - for idx in 1..<1000 { - let newItem = MoodEntry(context: viewContext) - newItem.timestamp = Date() - newItem.moodValue = Int16.random(in: 3 ... 4) - if Int16.random(in: 0 ... 400) % 5 == 0 { - newItem.moodValue = Int16.random(in: 0 ... 4) - } - newItem.canEdit = true - newItem.canDelete = true - - let date = Calendar.current.date(byAdding: .day, value: -idx, to: Date())! - newItem.forDate = date - newItem.weekDay = Int16(Calendar.current.component(.weekday, from: date)) - } - - saveAndRunDataListerners() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - - func longestStreak() -> [MoodEntry] { - // let predicate = NSPredicate(format: "forDate == %@", date as NSDate) - - let fetchRequest = NSFetchRequest(entityName: "MoodEntry") - // fetchRequest.predicate = predicate - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)] - let data = try! viewContext.fetch(fetchRequest) - return data - } -} diff --git a/Shared/Persisence/PersistenceUPDATE.swift b/Shared/Persisence/PersistenceUPDATE.swift deleted file mode 100644 index 1cd401e..0000000 --- a/Shared/Persisence/PersistenceUPDATE.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// PersistenceUPDATE.swift -// Feels -// -// Created by Trey Tartt on 2/18/22. -// - -import Foundation - -extension PersistenceController { - @discardableResult - public func update(entryDate: Date, withModd mood: Mood) -> Bool { - guard let existingEntry = getEntry(byDate: entryDate) else { - return false - } - - existingEntry.setValue(mood.rawValue, forKey: "moodValue") - saveAndRunDataListerners() - - EventLogger.log(event: "update_entry") - - return true - } -} diff --git a/Shared/Persisence/SharedModelContainer.swift b/Shared/Persisence/SharedModelContainer.swift new file mode 100644 index 0000000..ddc1d80 --- /dev/null +++ b/Shared/Persisence/SharedModelContainer.swift @@ -0,0 +1,79 @@ +// +// SharedModelContainer.swift +// Feels +// +// Factory for creating ModelContainer shared between main app and widget extension. +// + +import Foundation +import SwiftData + +enum SharedModelContainer { + /// Creates a ModelContainer with the appropriate configuration for app group sharing + /// - Parameter useCloudKit: Whether to enable CloudKit sync + /// - Returns: Configured ModelContainer + static func create(useCloudKit: Bool = false) -> ModelContainer { + let schema = Schema([MoodEntryModel.self]) + let storeURL = Self.storeURL + + let configuration: ModelConfiguration + if useCloudKit { + // CloudKit-enabled configuration + configuration = ModelConfiguration( + schema: schema, + url: storeURL, + cloudKitDatabase: .private(cloudKitContainerID) + ) + } else { + // Local-only configuration + configuration = ModelConfiguration( + schema: schema, + url: storeURL, + cloudKitDatabase: .none + ) + } + + do { + return try ModelContainer(for: schema, configurations: [configuration]) + } catch { + fatalError("Failed to create ModelContainer: \(error)") + } + } + + /// The URL for the SwiftData store in the shared app group container + static var storeURL: URL { + guard let containerURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: appGroupID + ) else { + fatalError("App Group container not available for: \(appGroupID)") + } + return containerURL.appendingPathComponent(storeFileName) + } + + /// App Group identifier based on build configuration + static var appGroupID: String { + #if DEBUG + return Constants.groupShareIdDebug + #else + return Constants.groupShareId + #endif + } + + /// CloudKit container identifier based on build configuration + static var cloudKitContainerID: String { + #if DEBUG + return "iCloud.com.tt.ifeelDebug" + #else + return "iCloud.com.tt.ifeel" + #endif + } + + /// Store file name based on build configuration + static var storeFileName: String { + #if DEBUG + return "Feels-Debug.store" + #else + return "Feels.store" + #endif + } +} diff --git a/Shared/Protocols/ChartDataBuildable.swift b/Shared/Protocols/ChartDataBuildable.swift index 9b4854b..d10c596 100644 --- a/Shared/Protocols/ChartDataBuildable.swift +++ b/Shared/Protocols/ChartDataBuildable.swift @@ -13,21 +13,21 @@ typealias Month = Int protocol ChartDataBuildable { associatedtype ChartType: ChartViewItemBuildable // [Year: [Month: [View]] - func buildGridData(withData data: [MoodEntry]) -> [Year: [Month: [ChartType]]] + func buildGridData(withData data: [MoodEntryModel]) -> [Year: [Month: [ChartType]]] } extension ChartDataBuildable { - public func buildGridData(withData data: [MoodEntry]) -> [Year: [Month: [ChartType]]] { + public func buildGridData(withData data: [MoodEntryModel]) -> [Year: [Month: [ChartType]]] { var returnData = [Int: [Int: [ChartType]]]() - + if let earliestEntry = data.first, let lastEntry = data.last { - + let calendar = Calendar.current - let components = calendar.dateComponents([.year], from: earliestEntry.forDate!) + let components = calendar.dateComponents([.year], from: earliestEntry.forDate) let earliestYear = components.year! - - let latestComponents = calendar.dateComponents([.year], from: lastEntry.forDate!) + + let latestComponents = calendar.dateComponents([.year], from: lastEntry.forDate) let latestYear = latestComponents.year! for year in earliestYear...latestYear { @@ -49,10 +49,10 @@ extension ChartDataBuildable { components.month = month components.year = year let startDateOfMonth = Calendar.current.date(from: components)! - + let items = data.filter({ entry in let components = calendar.dateComponents([.month, .year], from: startDateOfMonth) - let entryComponents = calendar.dateComponents([.month, .year], from: entry.forDate!) + let entryComponents = calendar.dateComponents([.month, .year], from: entry.forDate) return (components.month == entryComponents.month && components.year == entryComponents.year) }) @@ -64,16 +64,16 @@ extension ChartDataBuildable { return returnData } - private func createViewFor(monthEntries: [MoodEntry], forMonth month: Date) -> [ChartType] { + private func createViewFor(monthEntries: [MoodEntryModel], forMonth month: Date) -> [ChartType] { var filledOutArray = [ChartType]() - + let calendar = Calendar.current let range = calendar.range(of: .day, in: .month, for: month)! let numDays = range.count - + for day in 1...numDays { if let item = monthEntries.filter({ entry in - let components = calendar.dateComponents([.day], from: entry.forDate!) + let components = calendar.dateComponents([.day], from: entry.forDate) let date = components.day return day == date }).first { diff --git a/Shared/Random.swift b/Shared/Random.swift index 55669cf..2dff367 100644 --- a/Shared/Random.swift +++ b/Shared/Random.swift @@ -7,6 +7,7 @@ import Foundation import SwiftUI +import SwiftData struct Constants { static let groupShareId = "group.com.tt.ifeel" @@ -70,25 +71,25 @@ class Random { return newValue } - static func createTotalPerc(fromEntries entries: [MoodEntry]) -> [MoodMetrics] { + static func createTotalPerc(fromEntries entries: [MoodEntryModel]) -> [MoodMetrics] { let filteredEntries = entries.filter({ return ![.missing, .placeholder].contains($0.mood) }) var returnData = [MoodMetrics]() - + for (_, mood) in Mood.allValues.enumerated() { let moodEntries = filteredEntries.filter({ - Int($0.moodValue) == mood.rawValue + $0.moodValue == mood.rawValue }) let total = moodEntries.count let perc = (Float(total) / Float(filteredEntries.count)) * 100 returnData.append(MoodMetrics(mood: mood, total: total, percent: perc)) } - + returnData = returnData.sorted(by: { $0.mood.rawValue > $1.mood.rawValue }) - + return returnData } } diff --git a/Shared/ShowBasedOnVoteLogics.swift b/Shared/ShowBasedOnVoteLogics.swift index eeaaeb0..1a88a0b 100644 --- a/Shared/ShowBasedOnVoteLogics.swift +++ b/Shared/ShowBasedOnVoteLogics.swift @@ -5,13 +5,13 @@ // Created by Trey Tartt on 2/17/22. // -import CoreData import SwiftUI +import SwiftData /* current day 3/5/22 - + ..... before time | after time ..... day option = .today @@ -19,24 +19,25 @@ import SwiftUI voting for 3/4 | voting for 3/5 ------------------------*------------------------- db should contain 3/3 | db should contain 3/4 - + ---------------------------------------------------------------------------- - + day option = .yesterday -------- voting for 3/3 | voting for 3/4 ------------------------*------------------------- db should contain 3/2 | db should contain 3/3 - + */ +@MainActor class ShowBasedOnVoteLogics { static private func returnCurrentVoteStatus(onboardingData: OnboardingData) -> (passedTimeToVote: Bool, inputDay: DayOptions) { let passedTimeToVote = ShowBasedOnVoteLogics.passedTodaysVotingUnlock(voteDate: onboardingData.date) let inputDay: DayOptions = onboardingData.inputDay - + return (passedTimeToVote, inputDay) } - + static public func passedTodaysVotingUnlock(voteDate: Date) -> Bool { let currentDateComp = Calendar.current.dateComponents([.hour, .minute], from: Date()) let savedDateComp = Calendar.current.dateComponents([.hour, .minute], from: voteDate) @@ -45,32 +46,25 @@ class ShowBasedOnVoteLogics { let currentMin = currentDateComp.minute, let savedHour = savedDateComp.hour, let savedMin = savedDateComp.minute { - + if currentHour > savedHour { return true } - + if currentHour == savedHour { return currentMin >= savedMin } } - + return false } - - static public func isMissingCurrentVote(onboardingData: OnboardingData) -> Bool { + + static public func isMissingCurrentVote(onboardingData: OnboardingData) -> Bool { let startDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboardingData).startOfDay let endDate = startDate.endOfDay - let fetchRequest = NSFetchRequest(entityName: "MoodEntry") - let fromPredicate = NSPredicate(format: "%@ <= %K", startDate - as NSDate, #keyPath(MoodEntry.forDate)) - let toPredicate = NSPredicate(format: "%K < %@", #keyPath(MoodEntry.forDate), endDate as NSDate) - let datePredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fromPredicate, toPredicate]) - fetchRequest.predicate = datePredicate - let entries = try! PersistenceController.shared.viewContext.count(for: fetchRequest) - - return entries < 1 + let entry = DataController.shared.getEntry(byDate: startDate) + return entry == nil || entry?.mood == .missing } static public func getCurrentVotingDate(onboardingData: OnboardingData) -> Date { diff --git a/Shared/Stats.swift b/Shared/Stats.swift index 5cbc2e9..8f14b51 100644 --- a/Shared/Stats.swift +++ b/Shared/Stats.swift @@ -8,7 +8,7 @@ import Foundation class Stats { - static func getCountFor(moodType: Mood, inData data: [MoodEntry]) -> Int { + static func getCountFor(moodType: Mood, inData data: [MoodEntryModel]) -> Int { let num = data.filter({ $0.moodValue == moodType.rawValue }).count diff --git a/Shared/Views/AddMoodHeaderView.swift b/Shared/Views/AddMoodHeaderView.swift index 72fc21b..f450104 100644 --- a/Shared/Views/AddMoodHeaderView.swift +++ b/Shared/Views/AddMoodHeaderView.swift @@ -7,7 +7,7 @@ import Foundation import SwiftUI -import CoreData +import SwiftData struct AddMoodHeaderView: View { @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system @@ -255,11 +255,11 @@ struct AddMoodHeaderView_Previews: PreviewProvider { Group { AddMoodHeaderView(addItemHeaderClosure: { (_,_) in - }).environment(\.managedObjectContext, PersistenceController.shared.viewContext) + }).modelContainer(DataController.shared.container) AddMoodHeaderView(addItemHeaderClosure: { (_,_) in - }).preferredColorScheme(.dark).environment(\.managedObjectContext, PersistenceController.shared.viewContext) + }).preferredColorScheme(.dark).modelContainer(DataController.shared.container) } } } diff --git a/Shared/Views/BGView.swift b/Shared/Views/BGView.swift index 6babd7d..d0108c1 100644 --- a/Shared/Views/BGView.swift +++ b/Shared/Views/BGView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import SwiftData struct BGViewItem: View { @AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default @@ -81,13 +82,13 @@ struct BGView: View, Equatable { struct BGView_Previews: PreviewProvider { static var previews: some View { - BGView().environment(\.managedObjectContext, PersistenceController.shared.viewContext) + BGView().modelContainer(DataController.shared.container) .onAppear(perform: { - PersistenceController.shared.populateMemory() + DataController.shared.populateMemory() }) - + BGView() .preferredColorScheme(.dark) - .environment(\.managedObjectContext, PersistenceController.shared.viewContext) + .modelContainer(DataController.shared.container) } } diff --git a/Shared/Views/DayView/DayView.swift b/Shared/Views/DayView/DayView.swift index 8c49ee0..48b6b86 100644 --- a/Shared/Views/DayView/DayView.swift +++ b/Shared/Views/DayView/DayView.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CoreData +import SwiftData import Charts struct DayViewConstants { @@ -15,8 +15,6 @@ struct DayViewConstants { } struct DayView: View { - @Environment(\.managedObjectContext) private var viewContext - @AppStorage(UserDefaultsStore.Keys.deleteEnable.rawValue, store: GroupUserDefaults.groupDefaults) private var deleteEnabled = true @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system @@ -30,7 +28,7 @@ struct DayView: View { // MARK: edit row properties @State private var showingSheet = false - @State private var selectedEntry: MoodEntry? + @State private var selectedEntry: MoodEntryModel? // // MARK: ?? properties @@ -100,7 +98,7 @@ struct DayView: View { } .padding([.leading, .trailing]) .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in - PersistenceController.shared.fillInMissingDates() + DataController.shared.fillInMissingDates() viewModel.updateData() } .background( @@ -164,13 +162,13 @@ extension DayView { .background(.ultraThinMaterial) } - private func monthListView(month: Int, year: Int, entries: [MoodEntry]) -> some View { + private func monthListView(month: Int, year: Int, entries: [MoodEntryModel]) -> some View { VStack(spacing: 12) { // for reach all entries ForEach(entries.sorted(by: { - return $0.forDate! > $1.forDate! + return $0.forDate > $1.forDate }), id: \.self) { entry in - if filteredDays.currentFilters.contains(Int(entry.weekDay)) { + if filteredDays.currentFilters.contains(entry.weekDay) { EntryListView(entry: entry) .contentShape(Rectangle()) .onTapGesture(perform: { @@ -195,13 +193,14 @@ struct ViewOffsetKey: PreferenceKey { struct DayView_Previews: PreviewProvider { static var previews: some View { - DayView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: false)).environment(\.managedObjectContext, PersistenceController.shared.viewContext) + DayView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: false)) + .modelContainer(DataController.shared.container) .onAppear(perform: { - PersistenceController.shared.populateMemory() + DataController.shared.populateMemory() }) DayView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: false)) .preferredColorScheme(.dark) - .environment(\.managedObjectContext, PersistenceController.shared.viewContext) + .modelContainer(DataController.shared.container) } } diff --git a/Shared/Views/DayView/DayViewViewModel.swift b/Shared/Views/DayView/DayViewViewModel.swift index bf34740..1d4b456 100644 --- a/Shared/Views/DayView/DayViewViewModel.swift +++ b/Shared/Views/DayView/DayViewViewModel.swift @@ -6,18 +6,19 @@ // import SwiftUI -import CoreData +import SwiftData +@MainActor class DayViewViewModel: ObservableObject { - @Published var grouped = [Int: [Int: [MoodEntry]]]() + @Published var grouped = [Int: [Int: [MoodEntryModel]]]() @Published var numberOfItems = 0 - + let addMonthStartWeekdayPadding: Bool - + var hasNoData: Bool { grouped.isEmpty } - + private var numberOfEntries: Int { var num = 0 grouped.keys.forEach({ @@ -28,31 +29,25 @@ class DayViewViewModel: ObservableObject { }) }) return num - -// grouped.keys.map{ -// grouped[$0]!.values.reduce(0) { sum, array in -// sum + array.count -// } -// }.reduce(0, +) } - + init(addMonthStartWeekdayPadding: Bool) { self.addMonthStartWeekdayPadding = addMonthStartWeekdayPadding - - PersistenceController.shared.switchContainerListeners.append { + + DataController.shared.switchContainerListeners.append { self.getGroupedData(addMonthStartWeekdayPadding: self.addMonthStartWeekdayPadding) } - PersistenceController.shared.addNewDataListener { + DataController.shared.addNewDataListener { withAnimation{ self.updateData() } } updateData() } - + private func getGroupedData(addMonthStartWeekdayPadding: Bool) { - var newStuff = PersistenceController.shared.splitIntoYearMonth(includedDays: [1,2,3,4,5,6,7]) + var newStuff = DataController.shared.splitIntoYearMonth(includedDays: [1,2,3,4,5,6,7]) if addMonthStartWeekdayPadding { newStuff = MoodEntryFunctions.padMoodEntriesForCalendar(entries: newStuff) } @@ -63,65 +58,55 @@ class DayViewViewModel: ObservableObject { public func updateData() { getGroupedData(addMonthStartWeekdayPadding: self.addMonthStartWeekdayPadding) } - + public func add(mood: Mood, forDate date: Date, entryType: EntryType) { - PersistenceController.shared.add(mood: mood, forDate: date, entryType: entryType) + DataController.shared.add(mood: mood, forDate: date, entryType: entryType) } - - public func update(entry: MoodEntry, toMood mood: Mood) { - if !PersistenceController.shared.update(entryDate: entry.forDate!, withModd: mood) { + + public func update(entry: MoodEntryModel, toMood mood: Mood) { + if !DataController.shared.update(entryDate: entry.forDate, withMood: mood) { #warning("show error") } } - + 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! + $0.forDate > $1.forDate }) - var entriesToDelete = [MoodEntry]() + var entriesToDelete = [MoodEntryModel]() for idx in offsets { let obj = mutableEntries.remove(at: idx) entriesToDelete.append(obj) } entriesToDelete.forEach({ entry in - let entryDate = entry.forDate! - PersistenceController.shared.viewContext.delete(entry) + let entryDate = entry.forDate + DataController.shared.modelContext.delete(entry) self.add(mood: .missing, forDate: entryDate, entryType: .listView) }) } - - do { - try PersistenceController.shared.viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } + + DataController.shared.save() } - - static func updateTitleHeader(forEntry entry: MoodEntry?) -> String { + + static func updateTitleHeader(forEntry entry: MoodEntryModel?) -> String { guard let entry = entry else { return "" } - - guard let forDate = entry.forDate else { - return "" - } - + + let forDate = entry.forDate + let components = Calendar.current.dateComponents([.day, .month, .year], from: forDate) - // let day = components.day! let month = components.month! let year = components.year! - + let monthName = Random.monthName(fromMonthInt: month) - let weekday = Random.weekdayName(fromDate:entry.forDate!) - let dayz = Random.dayFormat(fromDate:entry.forDate!) - + 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) } } diff --git a/Shared/Views/EntryListView.swift b/Shared/Views/EntryListView.swift index 758b63c..f953146 100644 --- a/Shared/Views/EntryListView.swift +++ b/Shared/Views/EntryListView.swift @@ -13,7 +13,7 @@ struct EntryListView: View { @AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor @Environment(\.colorScheme) private var colorScheme - public let entry: MoodEntry + public let entry: MoodEntryModel private var moodColor: Color { moodTint.color(forMood: entry.mood) @@ -53,14 +53,14 @@ struct EntryListView: View { VStack(alignment: .leading, spacing: 6) { // Date row HStack(spacing: 6) { - Text(Random.weekdayName(fromDate: entry.forDate!)) + Text(Random.weekdayName(fromDate: entry.forDate)) .font(.system(size: 17, weight: .semibold)) .foregroundColor(textColor) Text("•") .foregroundColor(textColor.opacity(0.4)) - Text(Random.dayFormat(fromDate: entry.forDate!)) + Text(Random.dayFormat(fromDate: entry.forDate)) .font(.system(size: 17, weight: .medium)) .foregroundColor(textColor.opacity(0.8)) } @@ -115,7 +115,7 @@ struct EntryListView: View { } struct EntryListView_Previews: PreviewProvider { - static let fakeData = PersistenceController.shared.randomEntries(count: 1).first! + @MainActor static let fakeData = DataController.shared.randomEntries(count: 1).first! static var previews: some View { VStack(spacing: 8) { diff --git a/Shared/Views/GraphView.swift b/Shared/Views/GraphView.swift index e62c58d..c49d27c 100644 --- a/Shared/Views/GraphView.swift +++ b/Shared/Views/GraphView.swift @@ -7,7 +7,6 @@ import Foundation import SwiftUI -import CoreData import Charts struct GraphView: View { diff --git a/Shared/Views/HeaderPercView.swift b/Shared/Views/HeaderPercView.swift index 537a80c..aeda1b6 100644 --- a/Shared/Views/HeaderPercView.swift +++ b/Shared/Views/HeaderPercView.swift @@ -23,18 +23,19 @@ struct HeaderPercView: View { let backDays: Int let type: PercViewType + @MainActor init(fakeData: Bool, backDays: Int, type: PercViewType) { self.type = type self.backDays = backDays - var moodEntries: [MoodEntry]? + var moodEntries: [MoodEntryModel]? if fakeData { - moodEntries = PersistenceController.shared.randomEntries(count: 10) + 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 = PersistenceController.shared.getData(startDate: daysAgo, endDate: Date(), includedDays: [1,2,3,4,5,6,7]) + + moodEntries = DataController.shared.getData(startDate: daysAgo, endDate: Date(), includedDays: [1,2,3,4,5,6,7]) } if let moodEntries = moodEntries { diff --git a/Shared/Views/HeaderStatsView.swift b/Shared/Views/HeaderStatsView.swift index eaf17cb..679d0cf 100644 --- a/Shared/Views/HeaderStatsView.swift +++ b/Shared/Views/HeaderStatsView.swift @@ -24,16 +24,16 @@ struct HeaderStatsView : UIViewRepresentable { } self.tmpHolderToMakeViewDiffefrent = Color.random() entries = [BarChartDataEntry]() - - var moodEntries: [MoodEntry]? + + var moodEntries: [MoodEntryModel]? if fakeData { - moodEntries = PersistenceController.shared.randomEntries(count: 10) + 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 = PersistenceController.shared.getData(startDate: daysAgo, endDate: Date(), includedDays: [1,2,3,4,5,6,7]) + + moodEntries = DataController.shared.getData(startDate: daysAgo, endDate: Date(), includedDays: [1,2,3,4,5,6,7]) } if let moodEntries = moodEntries { for (index, mood) in Mood.allValues.enumerated() { diff --git a/Shared/Views/InsightsView/InsightsViewModel.swift b/Shared/Views/InsightsView/InsightsViewModel.swift index 5b00cfd..ac0191f 100644 --- a/Shared/Views/InsightsView/InsightsViewModel.swift +++ b/Shared/Views/InsightsView/InsightsViewModel.swift @@ -16,6 +16,7 @@ struct Insight: Identifiable { let mood: Mood? } +@MainActor class InsightsViewModel: ObservableObject { @Published var monthInsights: [Insight] = [] @Published var yearInsights: [Insight] = [] @@ -36,9 +37,9 @@ class InsightsViewModel: ObservableObject { let allTimeStart = Date(timeIntervalSince1970: 0) // Fetch entries for each period - let monthEntries = PersistenceController.shared.getData(startDate: monthStart, endDate: now, includedDays: [1,2,3,4,5,6,7]) - let yearEntries = PersistenceController.shared.getData(startDate: yearStart, endDate: now, includedDays: [1,2,3,4,5,6,7]) - let allTimeEntries = PersistenceController.shared.getData(startDate: allTimeStart, endDate: now, includedDays: [1,2,3,4,5,6,7]) + let monthEntries = DataController.shared.getData(startDate: monthStart, endDate: now, includedDays: [1,2,3,4,5,6,7]) + let yearEntries = DataController.shared.getData(startDate: yearStart, endDate: now, includedDays: [1,2,3,4,5,6,7]) + let allTimeEntries = DataController.shared.getData(startDate: allTimeStart, endDate: now, includedDays: [1,2,3,4,5,6,7]) // Generate insights for each period monthInsights = generateRandomInsights(entries: monthEntries, periodName: "this month", count: 5) @@ -46,7 +47,7 @@ class InsightsViewModel: ObservableObject { allTimeInsights = generateRandomInsights(entries: allTimeEntries, periodName: "all time", count: 5) } - private func generateRandomInsights(entries: [MoodEntry], periodName: String, count: Int) -> [Insight] { + private func generateRandomInsights(entries: [MoodEntryModel], periodName: String, count: Int) -> [Insight] { // Filter out missing/placeholder entries let validEntries = entries.filter { ![.missing, .placeholder].contains($0.mood) } @@ -85,7 +86,7 @@ class InsightsViewModel: ObservableObject { } // MARK: - Most Common Mood - private func generateMostCommonMoodInsights(entries: [MoodEntry], periodName: String) -> [Insight] { + private func generateMostCommonMoodInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let moodCounts = Dictionary(grouping: entries, by: { $0.mood }).mapValues { $0.count } @@ -120,7 +121,7 @@ class InsightsViewModel: ObservableObject { } // MARK: - Least Common Mood - private func generateLeastCommonMoodInsights(entries: [MoodEntry], periodName: String) -> [Insight] { + private func generateLeastCommonMoodInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let moodCounts = Dictionary(grouping: entries, by: { $0.mood }).mapValues { $0.count } @@ -143,7 +144,7 @@ class InsightsViewModel: ObservableObject { } // MARK: - Best Day of Week - private func generateBestDayInsights(entries: [MoodEntry], periodName: String) -> [Insight] { + private func generateBestDayInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let weekdayCounts = Dictionary(grouping: entries, by: { Int($0.weekDay) }) var weekdayScores: [Int: Float] = [:] @@ -176,7 +177,7 @@ class InsightsViewModel: ObservableObject { } // MARK: - Worst Day of Week - private func generateWorstDayInsights(entries: [MoodEntry], periodName: String) -> [Insight] { + private func generateWorstDayInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let weekdayCounts = Dictionary(grouping: entries, by: { Int($0.weekDay) }) var weekdayScores: [Int: Float] = [:] @@ -208,7 +209,7 @@ class InsightsViewModel: ObservableObject { } // MARK: - Streak Insights - private func generateStreakInsights(entries: [MoodEntry], periodName: String) -> [Insight] { + private func generateStreakInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let streakInfo = calculateStreaks(entries: entries) @@ -271,12 +272,12 @@ class InsightsViewModel: ObservableObject { } // MARK: - Trend Insights - private func generateTrendInsights(entries: [MoodEntry], periodName: String) -> [Insight] { + private func generateTrendInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] guard entries.count >= 4 else { return insights } - let sortedEntries = entries.sorted { $0.forDate! < $1.forDate! } + let sortedEntries = entries.sorted { $0.forDate < $1.forDate } let halfCount = sortedEntries.count / 2 let firstHalf = Array(sortedEntries.prefix(halfCount)) @@ -330,7 +331,7 @@ class InsightsViewModel: ObservableObject { } // MARK: - Positivity Insights - private func generatePositivityInsights(entries: [MoodEntry], periodName: String) -> [Insight] { + private func generatePositivityInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let positiveDays = entries.filter { [.great, .good].contains($0.mood) }.count @@ -393,7 +394,7 @@ class InsightsViewModel: ObservableObject { } // MARK: - Weekend vs Weekday - private func generateWeekendVsWeekdayInsights(entries: [MoodEntry], periodName: String) -> [Insight] { + private func generateWeekendVsWeekdayInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let weekendDays = entries.filter { [1, 7].contains(Int($0.weekDay)) } // Sunday = 1, Saturday = 7 @@ -431,12 +432,12 @@ class InsightsViewModel: ObservableObject { } // MARK: - Mood Swing Insights - private func generateMoodSwingInsights(entries: [MoodEntry], periodName: String) -> [Insight] { + private func generateMoodSwingInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] guard entries.count >= 3 else { return insights } - let sortedEntries = entries.sorted { $0.forDate! < $1.forDate! } + let sortedEntries = entries.sorted { $0.forDate < $1.forDate } var swings = 0 for i in 1.. [Insight] { + private func generateConsistencyInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let totalDaysInPeriod = calculateDaysInPeriod(periodName: periodName) @@ -535,7 +536,7 @@ class InsightsViewModel: ObservableObject { } // MARK: - Milestone Insights - private func generateMilestoneInsights(entries: [MoodEntry], periodName: String) -> [Insight] { + private func generateMilestoneInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let count = entries.count @@ -557,7 +558,7 @@ class InsightsViewModel: ObservableObject { } // MARK: - Comparative Insights - private func generateComparativeInsights(entries: [MoodEntry], periodName: String) -> [Insight] { + private func generateComparativeInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let moodCounts = Dictionary(grouping: entries, by: { $0.mood }).mapValues { $0.count } @@ -606,7 +607,7 @@ class InsightsViewModel: ObservableObject { } // MARK: - Pattern Insights - private func generatePatternInsights(entries: [MoodEntry], periodName: String) -> [Insight] { + private func generatePatternInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] // Monday blues check @@ -662,7 +663,7 @@ class InsightsViewModel: ObservableObject { } // MARK: - Fun Fact Insights - private func generateFunFactInsights(entries: [MoodEntry], periodName: String) -> [Insight] { + private func generateFunFactInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let greatDays = entries.filter { $0.mood == .great }.count @@ -688,7 +689,7 @@ class InsightsViewModel: ObservableObject { } // Calculate "great day streak potential" - let recentEntries = entries.sorted { $0.forDate! > $1.forDate! }.prefix(7) + let recentEntries = entries.sorted { $0.forDate > $1.forDate }.prefix(7) let recentGreat = recentEntries.filter { $0.mood == .great }.count if recentGreat >= 3 { insights.append(Insight( @@ -712,7 +713,7 @@ class InsightsViewModel: ObservableObject { } // MARK: - Motivational Insights - private func generateMotivationalInsights(entries: [MoodEntry], periodName: String) -> [Insight] { + private func generateMotivationalInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let avgMood = Float(entries.reduce(0) { $0 + Int($1.moodValue) }) / Float(entries.count) @@ -760,7 +761,7 @@ class InsightsViewModel: ObservableObject { } // MARK: - Rare Mood Insights - private func generateRareMoodInsights(entries: [MoodEntry], periodName: String) -> [Insight] { + private func generateRareMoodInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let moodCounts = Dictionary(grouping: entries, by: { $0.mood }).mapValues { $0.count } @@ -800,13 +801,13 @@ class InsightsViewModel: ObservableObject { } // MARK: - Recent Insights - private func generateRecentInsights(entries: [MoodEntry], periodName: String) -> [Insight] { + private func generateRecentInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] - let sortedEntries = entries.sorted { $0.forDate! > $1.forDate! } + let sortedEntries = entries.sorted { $0.forDate > $1.forDate } guard let mostRecent = sortedEntries.first else { return insights } - let daysSinceLastEntry = calendar.dateComponents([.day], from: mostRecent.forDate!, to: Date()).day ?? 0 + let daysSinceLastEntry = calendar.dateComponents([.day], from: mostRecent.forDate, to: Date()).day ?? 0 if daysSinceLastEntry == 0 { insights.append(Insight( @@ -858,13 +859,13 @@ class InsightsViewModel: ObservableObject { } // MARK: - Month of Year Insights - private func generateMonthOfYearInsights(entries: [MoodEntry], periodName: String) -> [Insight] { + private func generateMonthOfYearInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] guard periodName == "this year" || periodName == "all time" else { return insights } let monthGroups = Dictionary(grouping: entries) { entry -> Int in - calendar.component(.month, from: entry.forDate!) + calendar.component(.month, from: entry.forDate) } var monthScores: [Int: Float] = [:] @@ -899,7 +900,7 @@ class InsightsViewModel: ObservableObject { } // MARK: - Longest Mood Run - private func generateLongestMoodRunInsights(entries: [MoodEntry], periodName: String) -> [Insight] { + private func generateLongestMoodRunInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] for mood in [Mood.great, .good, .average, .bad, .horrible] { @@ -928,7 +929,7 @@ class InsightsViewModel: ObservableObject { } // MARK: - Average Mood Insights - private func generateAverageMoodInsights(entries: [MoodEntry], periodName: String) -> [Insight] { + private func generateAverageMoodInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let avgMood = Float(entries.reduce(0) { $0 + Int($1.moodValue) }) / Float(entries.count) @@ -980,8 +981,8 @@ class InsightsViewModel: ObservableObject { // MARK: - Helper Functions - private func calculateStreaks(entries: [MoodEntry]) -> (currentStreak: Int, longestStreak: Int) { - let sortedEntries = entries.sorted { $0.forDate! > $1.forDate! } + private func calculateStreaks(entries: [MoodEntryModel]) -> (currentStreak: Int, longestStreak: Int) { + let sortedEntries = entries.sorted { $0.forDate > $1.forDate } guard !sortedEntries.isEmpty else { return (0, 0) } var currentStreak = 0 @@ -989,14 +990,14 @@ class InsightsViewModel: ObservableObject { var tempStreak = 1 let today = calendar.startOfDay(for: Date()) - if let mostRecent = sortedEntries.first?.forDate, - calendar.isDate(mostRecent, inSameDayAs: today) || calendar.isDate(mostRecent, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) { + let mostRecent = sortedEntries.first!.forDate + if calendar.isDate(mostRecent, inSameDayAs: today) || calendar.isDate(mostRecent, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) { currentStreak = 1 var checkDate = calendar.date(byAdding: .day, value: -1, to: mostRecent)! for entry in sortedEntries.dropFirst() { - if let entryDate = entry.forDate, - calendar.isDate(entryDate, inSameDayAs: checkDate) { + let entryDate = entry.forDate + if calendar.isDate(entryDate, inSameDayAs: checkDate) { currentStreak += 1 checkDate = calendar.date(byAdding: .day, value: -1, to: checkDate)! } else { @@ -1006,15 +1007,14 @@ class InsightsViewModel: ObservableObject { } for i in 1.. (current: Int, longest: Int) { - let sortedEntries = entries.sorted { $0.forDate! < $1.forDate! } + private func calculateMoodStreaks(entries: [MoodEntryModel], moods: [Mood]) -> (current: Int, longest: Int) { + let sortedEntries = entries.sorted { $0.forDate < $1.forDate } guard !sortedEntries.isEmpty else { return (0, 0) } var currentStreak = 0 diff --git a/Shared/Views/MonthView/MonthDetailView.swift b/Shared/Views/MonthView/MonthDetailView.swift index 94b9f27..df8130b 100644 --- a/Shared/Views/MonthView/MonthDetailView.swift +++ b/Shared/Views/MonthView/MonthDetailView.swift @@ -14,15 +14,15 @@ struct MonthDetailView: View { @AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor @StateObject private var shareImage = StupidAssShareObservableObject() - + @State private var showingSheet = false - @State private var selectedEntry: MoodEntry? + @State private var selectedEntry: MoodEntryModel? @State private var showingUpdateEntryAlert = false @State private var showUpdateEntryAlert = false - + let monthInt: Int let yearInt: Int - @State var entries: [MoodEntry] + @State var entries: [MoodEntryModel] var parentViewModel: DayViewViewModel let columns = [ @@ -90,24 +90,24 @@ struct MonthDetailView: View { ForEach(Mood.allValues) { mood in Button(mood.strValue, action: { if let selectedEntry = selectedEntry { - PersistenceController.shared.update(entryDate: selectedEntry.forDate!, withModd: mood) + DataController.shared.update(entryDate: selectedEntry.forDate, withMood: mood) } updateEntries() showUpdateEntryAlert = false selectedEntry = nil }) } - + if let selectedEntry = selectedEntry, deleteEnabled, selectedEntry.mood != .missing { Button(String(localized: "content_view_delete_entry"), action: { - PersistenceController.shared.update(entryDate: selectedEntry.forDate!, withModd: .missing) + DataController.shared.update(entryDate: selectedEntry.forDate, withMood: .missing) updateEntries() showUpdateEntryAlert = false }) } - + Button(String(localized: "content_view_fill_in_missing_entry_cancel"), role: .cancel, action: { updateEntries() selectedEntry = nil @@ -161,13 +161,13 @@ struct MonthDetailView: View { } } - private func listViewEntry(forEntry entry: MoodEntry) -> some View { + private func listViewEntry(forEntry entry: MoodEntryModel) -> some View { VStack { if entry.mood == .placeholder { Text(" ") .font(.title3) .foregroundColor(Mood.placeholder.color) - + Circle() .frame(minWidth: 5, maxWidth: 50, @@ -176,11 +176,11 @@ struct MonthDetailView: View { alignment: .center) .foregroundColor(moodTint.color(forMood: entry.mood)) } else { - Text(entry.forDate!, + Text(entry.forDate, format: Date.FormatStyle().day()) .font(.title3) .foregroundColor(textColor) - + entry.mood.icon .resizable() .scaledToFit() @@ -193,11 +193,11 @@ struct MonthDetailView: View { } } } - + private func updateEntries() { parentViewModel.updateData() let (startDate, endDate) = Date.dateRange(monthInt: monthInt, yearInt: yearInt) - let updatedEntries = PersistenceController.shared.getData(startDate: startDate, endDate: endDate, includedDays: [1,2,3,4,5,6,7]) + let updatedEntries = DataController.shared.getData(startDate: startDate, endDate: endDate, includedDays: [1,2,3,4,5,6,7]) let padded = MoodEntryFunctions.padMoodEntriesMonth(monthEntries: updatedEntries) entries = padded } @@ -221,8 +221,8 @@ struct MonthDetailView: View { struct MonthDetailView_Previews: PreviewProvider { static var previews: some View { MonthDetailView(monthInt: 5, yearInt: 2022, entries: - PersistenceController.shared.randomEntries(count: 30).sorted(by: { - $0.forDate! < $1.forDate! + DataController.shared.randomEntries(count: 30).sorted(by: { + $0.forDate < $1.forDate }), parentViewModel: DayViewViewModel(addMonthStartWeekdayPadding: true)) } } diff --git a/Shared/Views/MonthView/MonthView.swift b/Shared/Views/MonthView/MonthView.swift index 8c0b4d2..8ee12bb 100644 --- a/Shared/Views/MonthView/MonthView.swift +++ b/Shared/Views/MonthView/MonthView.swift @@ -162,7 +162,7 @@ struct MonthView: View { struct MonthCard: View { let month: Int let year: Int - let entries: [MoodEntry] + let entries: [MoodEntryModel] let moodTint: MoodTints let imagePack: MoodImages let textColor: Color @@ -177,7 +177,7 @@ struct MonthCard: View { private var metrics: [MoodMetrics] { let (startDate, endDate) = Date.dateRange(monthInt: month, yearInt: year) - let monthEntries = PersistenceController.shared.getData(startDate: startDate, endDate: endDate, includedDays: [1,2,3,4,5,6,7]) + let monthEntries = DataController.shared.getData(startDate: startDate, endDate: endDate, includedDays: [1,2,3,4,5,6,7]) return Random.createTotalPerc(fromEntries: monthEntries) } @@ -250,7 +250,7 @@ struct MonthCard: View { // MARK: - Heatmap Cell struct HeatmapCell: View { - let entry: MoodEntry + let entry: MoodEntryModel let moodTint: MoodTints let isFiltered: Bool diff --git a/Shared/Views/SampleEntryView.swift b/Shared/Views/SampleEntryView.swift index 81c71a8..a516d29 100644 --- a/Shared/Views/SampleEntryView.swift +++ b/Shared/Views/SampleEntryView.swift @@ -8,14 +8,14 @@ import SwiftUI struct SampleEntryView: View { - @State private var sampleListEntry = PersistenceController.shared.generateObjectNotInArray(forDate: Date(), withMood: Mood.great) + @State private var sampleListEntry = DataController.shared.generateObjectNotInArray(forDate: Date(), withMood: Mood.great) @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system @AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor - + var body: some View { ZStack { theme.currentTheme.secondaryBGColor - + VStack { HStack { Spacer() @@ -24,7 +24,7 @@ struct SampleEntryView: View { .frame(width: 20, height: 20, alignment: .trailing) .foregroundColor(Color(UIColor.systemGray)) .onTapGesture { - sampleListEntry = PersistenceController.shared.generateObjectNotInArray(forDate: Date(), withMood: sampleListEntry.mood.next) + sampleListEntry = DataController.shared.generateObjectNotInArray(forDate: Date(), withMood: sampleListEntry.mood.next) } } Spacer() diff --git a/Shared/Views/SettingsView/SettingsView.swift b/Shared/Views/SettingsView/SettingsView.swift index 754f30c..55df88a 100644 --- a/Shared/Views/SettingsView/SettingsView.swift +++ b/Shared/Views/SettingsView/SettingsView.swift @@ -119,20 +119,14 @@ struct SettingsView: View { if columns.count != 7 { continue } - let moodEntry = MoodEntry(context: PersistenceController.shared.viewContext) - moodEntry.canDelete = Bool(columns[0])! - moodEntry.canEdit = Bool(columns[1])! - moodEntry.entryType = Int16(columns[2])! - moodEntry.forDate = dateFormatter.date(from: columns[3])! - moodEntry.moodValue = Int16(columns[4])! - moodEntry.timestamp = dateFormatter.date(from: columns[5])! - - let localTime = dateFormatter.date(from: columns[3])! - moodEntry.weekDay = Int16(Calendar.current.component(.weekday, from: localTime)) - // let _ = print("import info: ", columns[3], dateFormatter.date(from: columns[3]), localTime, Int16(Calendar.current.component(.weekday, from: localTime))) - try! PersistenceController.shared.viewContext.save() + let forDate = dateFormatter.date(from: columns[3])! + let moodValue = Int(columns[4])! + let mood = Mood(rawValue: moodValue) ?? .missing + let entryType = EntryType(rawValue: Int(columns[2])!) ?? .listView + + DataController.shared.add(mood: mood, forDate: forDate, entryType: entryType) } - PersistenceController.shared.saveAndRunDataListerners() + DataController.shared.saveAndRunDataListeners() EventLogger.log(event: "import_file") } else { EventLogger.log(event: "error_import_file") @@ -201,7 +195,7 @@ struct SettingsView: View { ZStack { theme.currentTheme.secondaryBGColor Button(action: { - PersistenceController.shared.populateTestData() + DataController.shared.populateTestData() }, label: { Text("Add test data") .foregroundColor(textColor) @@ -257,7 +251,7 @@ struct SettingsView: View { ZStack { theme.currentTheme.secondaryBGColor Button(action: { - PersistenceController.shared.clearDB() + DataController.shared.clearDB() }, label: { Text("Clear DB") .foregroundColor(textColor) @@ -267,12 +261,12 @@ struct SettingsView: View { .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } - + private var fixWeekday: some View { ZStack { theme.currentTheme.secondaryBGColor Button(action: { - PersistenceController.shared.fixWrongWeekdays() + DataController.shared.fixWrongWeekdays() }, label: { Text("Fix Weekday") .foregroundColor(textColor) @@ -576,28 +570,28 @@ struct SettingsView: View { struct TextFile: FileDocument { // tell the system we support only plain text static var readableContentTypes = [UTType.plainText] - + // by default our document is empty var text = "" - + // a simple initializer that creates new, empty documents + @MainActor init() { - let entries = PersistenceController.shared.getData(startDate: Date(timeIntervalSince1970: 0), - endDate: Date(), - includedDays: []) - + let entries = DataController.shared.getData(startDate: Date(timeIntervalSince1970: 0), + endDate: Date(), + includedDays: []) + var csvString = "canDelete,canEdit,entryType,forDate,moodValue,timestamp,weekDay\n" for entry in entries { let canDelete = entry.canDelete let canEdit = entry.canEdit let entryType = entry.entryType - let forDate = entry.forDate! + let forDate = entry.forDate let moodValue = entry.moodValue - let timestamp = entry.timestamp! + let timestamp = entry.timestamp let weekDay = entry.weekDay - + let dataString = "\(canDelete),\(canEdit),\(entryType),\(String(describing: forDate)),\(moodValue),\(String(describing:timestamp)),\(weekDay)\n" - // print("DATA: \(dataString)") csvString = csvString.appending(dataString) } text = csvString diff --git a/Shared/Views/Sharing/SharingListView.swift b/Shared/Views/Sharing/SharingListView.swift index 59a0e87..c3c0eda 100644 --- a/Shared/Views/Sharing/SharingListView.swift +++ b/Shared/Views/Sharing/SharingListView.swift @@ -34,16 +34,17 @@ struct SharingListView: View { @StateObject private var selectedShare = StupidAssObservableObject() var sharebleItems = [WrappedSharable]() + @MainActor init() { self.sharebleItems = [ WrappedSharable(preview: AnyView( AllMoodsTotalTemplate(isPreview: true, - startDate: PersistenceController.shared.earliestEntry?.forDate ?? Date(), + startDate: DataController.shared.earliestEntry?.forDate ?? Date(), endDate: Date(), fakeData: false) ),destination: AnyView( AllMoodsTotalTemplate(isPreview: false, - startDate: PersistenceController.shared.earliestEntry?.forDate ?? Date(), + startDate: DataController.shared.earliestEntry?.forDate ?? Date(), endDate: Date(), fakeData: false) ),description: AllMoodsTotalTemplate.description), @@ -62,12 +63,12 @@ struct SharingListView: View { ////////////////////////////////////////////////////////// WrappedSharable(preview: AnyView( LongestStreakTemplate(isPreview: true, - startDate: PersistenceController.shared.earliestEntry?.forDate ?? Date(), + startDate: DataController.shared.earliestEntry?.forDate ?? Date(), endDate: Date(), fakeData: false) ), destination: AnyView( LongestStreakTemplate(isPreview: false, - startDate: PersistenceController.shared.earliestEntry?.forDate ?? Date(), + startDate: DataController.shared.earliestEntry?.forDate ?? Date(), endDate: Date(), fakeData: false) ), description: LongestStreakTemplate.description), diff --git a/Shared/Views/SharingTemplates/AllMoodsTotalTemplate.swift b/Shared/Views/SharingTemplates/AllMoodsTotalTemplate.swift index 6affc63..f2a67b1 100644 --- a/Shared/Views/SharingTemplates/AllMoodsTotalTemplate.swift +++ b/Shared/Views/SharingTemplates/AllMoodsTotalTemplate.swift @@ -24,24 +24,25 @@ struct AllMoodsTotalTemplate: View, SharingTemplate { @StateObject private var shareImage = StupidAssShareObservableObject() private var entries = [MoodMetrics]() + + @MainActor init(isPreview: Bool, startDate: Date, endDate: Date, fakeData: Bool) { self.isPreview = isPreview self.startDate = startDate self.endDate = endDate - - var moodEntries: [MoodEntry]? - + + var moodEntries: [MoodEntryModel]? + if fakeData { - moodEntries = PersistenceController.shared.randomEntries(count: 10) + moodEntries = DataController.shared.randomEntries(count: 10) } else { - - moodEntries = PersistenceController.shared.getData(startDate:startDate, - endDate: endDate, - includedDays: [1,2,3,4,5,6,7]) + moodEntries = DataController.shared.getData(startDate:startDate, + endDate: endDate, + includedDays: [1,2,3,4,5,6,7]) } - + totalEntryCount = moodEntries?.count ?? 0 - + if let moodEntries = moodEntries { entries = Random.createTotalPerc(fromEntries: moodEntries) entries = entries.sorted(by: { diff --git a/Shared/Views/SharingTemplates/CurrentStreakTemplate.swift b/Shared/Views/SharingTemplates/CurrentStreakTemplate.swift index cb8efd7..73e7ea6 100644 --- a/Shared/Views/SharingTemplates/CurrentStreakTemplate.swift +++ b/Shared/Views/SharingTemplates/CurrentStreakTemplate.swift @@ -11,12 +11,12 @@ struct CurrentStreakTemplate: View, SharingTemplate { static var description: String { "Last 10 Days" } - + var isPreview: Bool var startDate: Date var endDate: Date - let moodEntries: [MoodEntry] - + let moodEntries: [MoodEntryModel] + @State var showSharingTemplate = false @StateObject private var shareImage = StupidAssShareObservableObject() @@ -32,24 +32,24 @@ struct CurrentStreakTemplate: View, SharingTemplate { GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center), GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center) ] - + + @MainActor init(isPreview: Bool, startDate: Date, endDate: Date, fakeData: Bool) { self.isPreview = isPreview self.startDate = startDate self.endDate = endDate - - var _moodEntries: [MoodEntry]? - + + var _moodEntries: [MoodEntryModel]? + if fakeData { - _moodEntries = PersistenceController.shared.randomEntries(count: 10) + _moodEntries = DataController.shared.randomEntries(count: 10) } else { - - _moodEntries = PersistenceController.shared.getData(startDate:startDate, - endDate: endDate, - includedDays: [1,2,3,4,5,6,7]) + _moodEntries = DataController.shared.getData(startDate:startDate, + endDate: endDate, + includedDays: [1,2,3,4,5,6,7]) } - - self.moodEntries = _moodEntries ?? [MoodEntry]() + + self.moodEntries = _moodEntries ?? [MoodEntryModel]() } var image: UIImage { diff --git a/Shared/Views/SharingTemplates/LongestStreakTemplate.swift b/Shared/Views/SharingTemplates/LongestStreakTemplate.swift index 71b7ebd..f427fc6 100644 --- a/Shared/Views/SharingTemplates/LongestStreakTemplate.swift +++ b/Shared/Views/SharingTemplates/LongestStreakTemplate.swift @@ -24,7 +24,7 @@ struct LongestStreakTemplate: View, SharingTemplate { var endDate: Date var fakeData: Bool - @State var moodEntries = [MoodEntry]() + @State var moodEntries = [MoodEntryModel]() @State var selectedMood: Mood = .great @State var showSharingTemplate = false @@ -57,28 +57,29 @@ struct LongestStreakTemplate: View, SharingTemplate { return image } + @MainActor private func configureData(fakeData: Bool, mood: Mood) { - var _moodEntries: [MoodEntry]? + var _moodEntries: [MoodEntryModel]? if fakeData { - _moodEntries = PersistenceController.shared.randomEntries(count: 10) + _moodEntries = DataController.shared.randomEntries(count: 10) } else { - _moodEntries = PersistenceController.shared.getData(startDate:startDate, - endDate: endDate, - includedDays: [1,2,3,4,5,6,7]) + _moodEntries = DataController.shared.getData(startDate:startDate, + endDate: endDate, + includedDays: [1,2,3,4,5,6,7]) } - let data = _moodEntries ?? [MoodEntry]() - + let data = _moodEntries ?? [MoodEntryModel]() + var splitArrays = createSubArrays(fromMoodEntries: data, splitOn: mood) splitArrays = splitArrays.sorted(by: { $0.count > $1.count } ) - self.moodEntries = splitArrays.first ?? [MoodEntry]() + self.moodEntries = splitArrays.first ?? [MoodEntryModel]() } - - private func createSubArrays(fromMoodEntries: [MoodEntry], splitOn: Mood) -> [[MoodEntry]] { - var splitArrays = [[MoodEntry]]() - var currentSplit = [MoodEntry]() + + private func createSubArrays(fromMoodEntries: [MoodEntryModel], splitOn: Mood) -> [[MoodEntryModel]] { + var splitArrays = [[MoodEntryModel]]() + var currentSplit = [MoodEntryModel]() for entry in fromMoodEntries { if entry.mood == splitOn { currentSplit.append(entry) diff --git a/Shared/Views/SharingTemplates/MonthTotalTemplate.swift b/Shared/Views/SharingTemplates/MonthTotalTemplate.swift index 7d44c3c..3afd9e8 100644 --- a/Shared/Views/SharingTemplates/MonthTotalTemplate.swift +++ b/Shared/Views/SharingTemplates/MonthTotalTemplate.swift @@ -28,8 +28,8 @@ struct MonthTotalTemplate: View, SharingTemplate { @AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = .white private var moodMetrics = [MoodMetrics]() - private var moodEntries = [MoodEntry]() - + private var moodEntries = [MoodEntryModel]() + let columns = [ GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center), GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center), @@ -39,30 +39,25 @@ struct MonthTotalTemplate: View, SharingTemplate { GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center), GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center) ] - + + @MainActor init(isPreview: Bool, startDate: Date, endDate: Date, fakeData: Bool) { self.isPreview = isPreview self.startDate = startDate self.endDate = endDate - - var _moodEntries: [MoodEntry]? - + + var _moodEntries: [MoodEntryModel]? + if fakeData { - _moodEntries = PersistenceController.shared.randomEntries(count: 10) + _moodEntries = DataController.shared.randomEntries(count: 10) } else { - - _moodEntries = PersistenceController.shared.getData(startDate: startDate, - endDate: endDate, - includedDays: [1,2,3,4,5,6,7]) - - // _moodEntries = PersistenceController.shared.getData(startDate:Calendar.current.date(byAdding: .day, value: -33, to: Date())!, - // endDate: Date(), - // includedDays: [1,2,3,4,5,6,7]) - + _moodEntries = DataController.shared.getData(startDate: startDate, + endDate: endDate, + includedDays: [1,2,3,4,5,6,7]) } - - moodEntries = _moodEntries ?? [MoodEntry]() - + + moodEntries = _moodEntries ?? [MoodEntryModel]() + totalEntryCount = moodEntries.count moodMetrics = Random.createTotalPerc(fromEntries: moodEntries) moodMetrics = moodMetrics.sorted(by: { diff --git a/Shared/Views/SmallRollUpHeaderView.swift b/Shared/Views/SmallRollUpHeaderView.swift index 39c223a..01b0a0a 100644 --- a/Shared/Views/SmallRollUpHeaderView.swift +++ b/Shared/Views/SmallRollUpHeaderView.swift @@ -10,18 +10,18 @@ import SwiftUI struct SmallRollUpHeaderView: View { @Binding var viewType: MainSwitchableViewType @AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default - + @AppStorage(UserDefaultsStore.Keys.shape.rawValue, store: GroupUserDefaults.groupDefaults) private var shape: BGShape = .circle - + @AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor - let entries: [MoodEntry] + let entries: [MoodEntryModel] private var moodMetrics = [MoodMetrics]() - - init(entries: [MoodEntry], viewType: Binding) { + + init(entries: [MoodEntryModel], viewType: Binding) { self.entries = entries self._viewType = viewType - + moodMetrics = Random.createTotalPerc(fromEntries: entries) } @@ -85,16 +85,16 @@ struct SmallRollUpHeaderView: View { struct SmallHeaderView_Previews: PreviewProvider { static var previews: some View { Group { - SmallRollUpHeaderView(entries: PersistenceController.shared.randomEntries(count: 10), + SmallRollUpHeaderView(entries: DataController.shared.randomEntries(count: 10), viewType: .constant(.total)) - - SmallRollUpHeaderView(entries: PersistenceController.shared.randomEntries(count: 10), + + SmallRollUpHeaderView(entries: DataController.shared.randomEntries(count: 10), viewType: .constant(.percentageShape)) .background(.gray) - - SmallRollUpHeaderView(entries: PersistenceController.shared.randomEntries(count: 10), + + SmallRollUpHeaderView(entries: DataController.shared.randomEntries(count: 10), viewType: .constant(.percentage)) - + .background(.gray) } } diff --git a/Shared/Views/YearView/YearView.swift b/Shared/Views/YearView/YearView.swift index 0cf3280..7f89104 100644 --- a/Shared/Views/YearView/YearView.swift +++ b/Shared/Views/YearView/YearView.swift @@ -6,17 +6,15 @@ // import SwiftUI -import CoreData +import SwiftData struct YearView: View { let months = [(0, "J"), (1, "F"), (2,"M"), (3,"A"), (4,"M"), (5, "J"), (6,"J"), (7,"A"), (8,"S"), (9,"O"), (10, "N"), (11,"D")] @State private var toggle = true - @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \MoodEntry.forDate, ascending: false)], - animation: .spring()) - private var items: FetchedResults + @Query(sort: \MoodEntryModel.forDate, order: .reverse) + private var items: [MoodEntryModel] @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system @AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default @@ -128,10 +126,10 @@ struct YearCard: View { private let months = ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"] private let heatmapColumns = Array(repeating: GridItem(.flexible(), spacing: 2), count: 12) - private var yearEntries: [MoodEntry] { + private var yearEntries: [MoodEntryModel] { let firstOfYear = Calendar.current.date(from: DateComponents(year: year, month: 1, day: 1))! let lastOfYear = Calendar.current.date(from: DateComponents(year: year + 1, month: 1, day: 1))! - return PersistenceController.shared.getData(startDate: firstOfYear, endDate: lastOfYear, includedDays: filteredDays) + return DataController.shared.getData(startDate: firstOfYear, endDate: lastOfYear, includedDays: filteredDays) } private var metrics: [MoodMetrics] { diff --git a/Shared/Views/YearView/YearViewModel.swift b/Shared/Views/YearView/YearViewModel.swift index 3a6a72d..92e1b82 100644 --- a/Shared/Views/YearView/YearViewModel.swift +++ b/Shared/Views/YearView/YearViewModel.swift @@ -7,41 +7,42 @@ import Foundation -class YearViewModel: ObservableObject { +@MainActor +class YearViewModel: ObservableObject { @Published public var entryStartDate: Date = Date() @Published public var entryEndDate: Date = Date() @Published var selectedDays = [Int]() - + // year, month, items @Published public private(set) var data = [Int: [Int: [DayChartView]]]() @Published public private(set) var numberOfRatings: Int = 0 - public private(set) var uncategorizedData = [MoodEntry]() { + public private(set) var uncategorizedData = [MoodEntryModel]() { didSet { self.numberOfRatings = uncategorizedData.count } } - + init() { updateData() } - + private func updateData() { - let filteredEntries = PersistenceController.shared.getData(startDate: Date(timeIntervalSince1970: 0), - endDate: Date(), - includedDays: selectedDays) - - if let fuckingDAte = filteredEntries.sorted(by: { $0.forDate! < $1.forDate! }).first?.forDate { - self.entryStartDate = fuckingDAte + let filteredEntries = DataController.shared.getData(startDate: Date(timeIntervalSince1970: 0), + endDate: Date(), + includedDays: selectedDays) + + if let firstDate = filteredEntries.sorted(by: { $0.forDate < $1.forDate }).first?.forDate { + self.entryStartDate = firstDate } self.entryEndDate = Date() } - + private let chartViewBuilder = DayChartViewChartBuilder() - + public func filterEntries(startDate: Date, endDate: Date) { - let filteredEntries = PersistenceController.shared.getData(startDate: startDate, - endDate: endDate, - includedDays: selectedDays) + let filteredEntries = DataController.shared.getData(startDate: startDate, + endDate: endDate, + includedDays: selectedDays) data.removeAll() let filledOutData = chartViewBuilder.buildGridData(withData: filteredEntries) data = filledOutData