Migrate from Core Data to SwiftData
- Replace Core Data with SwiftData for iOS 18+ - Create MoodEntryModel as @Model class replacing MoodEntry entity - Create SharedModelContainer for App Group container sharing - Create DataController with CRUD extensions replacing PersistenceController - Update all views and view models to use MoodEntryModel - Update widget extension to use SwiftData - Remove old Core Data files (Persistence*.swift, .xcdatamodeld) - Add EntryType enum with all entry type cases - Fix widget label truncation with proper spacing and text scaling 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,8 @@
|
|||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(find:*)",
|
"Bash(find:*)",
|
||||||
"Bash(xcodebuild:*)"
|
"Bash(xcodebuild:*)",
|
||||||
|
"Bash(cat:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,10 +92,9 @@
|
|||||||
"Color+Codable.swift",
|
"Color+Codable.swift",
|
||||||
"Date+Extensions.swift",
|
"Date+Extensions.swift",
|
||||||
EventLogger.swift,
|
EventLogger.swift,
|
||||||
Feels.xcdatamodeld,
|
|
||||||
Models/DiamondView.swift,
|
Models/DiamondView.swift,
|
||||||
Models/Mood.swift,
|
Models/Mood.swift,
|
||||||
Models/MoodEntryExtension.swift,
|
Models/MoodEntryModel.swift,
|
||||||
Models/MoodImagable.swift,
|
Models/MoodImagable.swift,
|
||||||
Models/MoodMetrics.swift,
|
Models/MoodMetrics.swift,
|
||||||
Models/MoodTintable.swift,
|
Models/MoodTintable.swift,
|
||||||
@@ -105,11 +104,12 @@
|
|||||||
Models/UserDefaultsStore.swift,
|
Models/UserDefaultsStore.swift,
|
||||||
Onboarding/OnboardingData.swift,
|
Onboarding/OnboardingData.swift,
|
||||||
Onboarding/views/OnboardingDay.swift,
|
Onboarding/views/OnboardingDay.swift,
|
||||||
Persisence/Persistence.swift,
|
Persisence/DataController.swift,
|
||||||
Persisence/PersistenceADD.swift,
|
Persisence/DataControllerADD.swift,
|
||||||
Persisence/PersistenceDELETE.swift,
|
Persisence/DataControllerDELETE.swift,
|
||||||
Persisence/PersistenceGET.swift,
|
Persisence/DataControllerGET.swift,
|
||||||
Persisence/PersistenceHelper.swift,
|
Persisence/DataControllerHelper.swift,
|
||||||
|
Persisence/SharedModelContainer.swift,
|
||||||
Random.swift,
|
Random.swift,
|
||||||
ShowBasedOnVoteLogics.swift,
|
ShowBasedOnVoteLogics.swift,
|
||||||
Views/BGView.swift,
|
Views/BGView.swift,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ struct VoteMoodIntent: AppIntent {
|
|||||||
let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding())
|
let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding())
|
||||||
|
|
||||||
// Add mood entry
|
// 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
|
// Store last voted date
|
||||||
let dateString = ISO8601DateFormatter().string(from: Calendar.current.startOfDay(for: votingDate))
|
let dateString = ISO8601DateFormatter().string(from: Calendar.current.startOfDay(for: votingDate))
|
||||||
@@ -63,19 +63,24 @@ struct VoteWidgetProvider: TimelineProvider {
|
|||||||
completion(entry)
|
completion(entry)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let entry = createEntry()
|
Task { @MainActor in
|
||||||
completion(entry)
|
let entry = createEntry()
|
||||||
|
completion(entry)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTimeline(in context: Context, completion: @escaping (Timeline<VoteWidgetEntry>) -> Void) {
|
func getTimeline(in context: Context, completion: @escaping (Timeline<VoteWidgetEntry>) -> Void) {
|
||||||
let entry = createEntry()
|
Task { @MainActor in
|
||||||
|
let entry = createEntry()
|
||||||
|
|
||||||
// Refresh at midnight
|
// Refresh at midnight
|
||||||
let midnight = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: Date())!)
|
let midnight = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: Date())!)
|
||||||
let timeline = Timeline(entries: [entry], policy: .after(midnight))
|
let timeline = Timeline(entries: [entry], policy: .after(midnight))
|
||||||
completion(timeline)
|
completion(timeline)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
private func createEntry() -> VoteWidgetEntry {
|
private func createEntry() -> VoteWidgetEntry {
|
||||||
let hasSubscription = GroupUserDefaults.groupDefaults.bool(forKey: UserDefaultsStore.Keys.hasActiveSubscription.rawValue)
|
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)!
|
let dayEnd = Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: dayStart)!
|
||||||
|
|
||||||
// Check if user has voted today
|
// 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
|
let hasVotedToday = todayEntry != nil && todayEntry?.mood != Mood.missing && todayEntry?.mood != Mood.placeholder
|
||||||
|
|
||||||
// Get today's mood if voted
|
// Get today's mood if voted
|
||||||
@@ -93,7 +98,7 @@ struct VoteWidgetProvider: TimelineProvider {
|
|||||||
// Get stats for display after voting
|
// Get stats for display after voting
|
||||||
var stats: MoodStats? = nil
|
var stats: MoodStats? = nil
|
||||||
if hasVotedToday {
|
if hasVotedToday {
|
||||||
let allEntries = PersistenceController.shared.getData(
|
let allEntries = DataController.shared.getData(
|
||||||
startDate: Date(timeIntervalSince1970: 0),
|
startDate: Date(timeIntervalSince1970: 0),
|
||||||
endDate: Date(),
|
endDate: Date(),
|
||||||
includedDays: []
|
includedDays: []
|
||||||
@@ -188,9 +193,10 @@ struct VotingView: View {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Horizontal layout for medium/large
|
// Horizontal layout for medium/large
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 4) {
|
||||||
ForEach(moods, id: \.rawValue) { mood in
|
ForEach(moods, id: \.rawValue) { mood in
|
||||||
MoodButton(mood: mood, isCompact: false)
|
MoodButton(mood: mood, isCompact: false)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,6 +230,8 @@ struct MoodButton: View {
|
|||||||
Text(mood.widgetDisplayName)
|
Text(mood.widgetDisplayName)
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.7)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import WidgetKit
|
import WidgetKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Intents
|
import Intents
|
||||||
import CoreData
|
import SwiftData
|
||||||
|
|
||||||
class WatchTimelineView: Identifiable {
|
class WatchTimelineView: Identifiable {
|
||||||
let id = UUID()
|
let id = UUID()
|
||||||
@@ -28,7 +28,7 @@ class WatchTimelineView: Identifiable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct TimeLineCreator {
|
struct TimeLineCreator {
|
||||||
static func createViews(daysBack: Int) -> [WatchTimelineView] {
|
@MainActor static func createViews(daysBack: Int) -> [WatchTimelineView] {
|
||||||
var timeLineView = [WatchTimelineView]()
|
var timeLineView = [WatchTimelineView]()
|
||||||
|
|
||||||
let latestDayToShow = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding())
|
let latestDayToShow = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding())
|
||||||
@@ -42,7 +42,7 @@ struct TimeLineCreator {
|
|||||||
let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable()
|
let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable()
|
||||||
let moodImages: MoodImagable.Type = UserDefaultsStore.moodMoodImagable()
|
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),
|
timeLineView.append(WatchTimelineView(image: moodImages.icon(forMood: todayEntry.mood),
|
||||||
graphic: moodImages.icon(forMood: todayEntry.mood),
|
graphic: moodImages.icon(forMood: todayEntry.mood),
|
||||||
date: dayStart,
|
date: dayStart,
|
||||||
@@ -99,7 +99,7 @@ struct Provider: IntentTimelineProvider {
|
|||||||
timeLineViews: TimeLineCreator.createSampleViews(count: 10))
|
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
|
// Use sample data for widget picker preview, real data otherwise
|
||||||
let timeLineViews: [WatchTimelineView]
|
let timeLineViews: [WatchTimelineView]
|
||||||
if context.isPreview {
|
if context.isPreview {
|
||||||
@@ -165,9 +165,6 @@ struct FeelsWidgetEntryView : View {
|
|||||||
@unknown default:
|
@unknown default:
|
||||||
fatalError()
|
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
|
@ViewBuilder
|
||||||
var body: some View {
|
var body: some View {
|
||||||
SmallGraphicWidgetView(entry: entry)
|
SmallGraphicWidgetView(entry: entry)
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange)) { _ in
|
|
||||||
// make sure you don't call this too often
|
|
||||||
WidgetCenter.shared.reloadAllTimelines()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,21 +10,15 @@ import UserNotifications
|
|||||||
import UIKit
|
import UIKit
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
// import Firebase // Firebase removed
|
|
||||||
|
|
||||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||||
private let savedOnboardingData = UserDefaultsStore.getOnboarding()
|
private let savedOnboardingData = UserDefaultsStore.getOnboarding()
|
||||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
@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 {
|
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||||
// PersistenceController.shared.clearDB()
|
DataController.shared.removeNoForDates()
|
||||||
// PersistenceController.shared.deleteLast(numberOfEntries: 5)
|
DataController.shared.fillInMissingDates()
|
||||||
// PersistenceController.shared.deleteRandomFromLast(numberOfEntries: 10)
|
|
||||||
// GroupUserDefaults.groupDefaults.set(false, forKey: UserDefaultsStore.Keys.showNSFW.rawValue)
|
|
||||||
|
|
||||||
// FirebaseApp.configure() // Firebase removed
|
|
||||||
PersistenceController.shared.removeNoForDates()
|
|
||||||
PersistenceController.shared.fillInMissingDates()
|
|
||||||
UNUserNotificationCenter.current().delegate = self
|
UNUserNotificationCenter.current().delegate = self
|
||||||
|
|
||||||
UIPageControl.appearance().currentPageIndicatorTintColor = UIColor(textColor)
|
UIPageControl.appearance().currentPageIndicatorTintColor = UIColor(textColor)
|
||||||
@@ -40,8 +34,9 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||||
PersistenceController.shared.fillInMissingDates()
|
DataController.shared.fillInMissingDates()
|
||||||
|
|
||||||
// reschedule notifications so there's a new title next notification
|
// reschedule notifications so there's a new title next notification
|
||||||
LocalNotification.rescheduleNotifiations()
|
LocalNotification.rescheduleNotifiations()
|
||||||
@@ -57,20 +52,21 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
|
|||||||
completionHandler([.badge, .banner, .sound])
|
completionHandler([.badge, .banner, .sound])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||||
if let action = LocalNotification.ActionType(rawValue: response.actionIdentifier) {
|
if let action = LocalNotification.ActionType(rawValue: response.actionIdentifier) {
|
||||||
let date = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: savedOnboardingData)
|
let date = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: savedOnboardingData)
|
||||||
switch action {
|
switch action {
|
||||||
case .horrible:
|
case .horrible:
|
||||||
PersistenceController.shared.add(mood: .horrible, forDate: date, entryType: .notification)
|
DataController.shared.add(mood: .horrible, forDate: date, entryType: .notification)
|
||||||
case .bad:
|
case .bad:
|
||||||
PersistenceController.shared.add(mood: .bad, forDate: date, entryType: .notification)
|
DataController.shared.add(mood: .bad, forDate: date, entryType: .notification)
|
||||||
case .average:
|
case .average:
|
||||||
PersistenceController.shared.add(mood: .average, forDate: date, entryType: .notification)
|
DataController.shared.add(mood: .average, forDate: date, entryType: .notification)
|
||||||
case .good:
|
case .good:
|
||||||
PersistenceController.shared.add(mood: .good, forDate: date, entryType: .notification)
|
DataController.shared.add(mood: .good, forDate: date, entryType: .notification)
|
||||||
case .great:
|
case .great:
|
||||||
PersistenceController.shared.add(mood: .great, forDate: date, entryType: .notification)
|
DataController.shared.add(mood: .great, forDate: date, entryType: .notification)
|
||||||
}
|
}
|
||||||
UIApplication.shared.applicationIconBadgeNumber = 0
|
UIApplication.shared.applicationIconBadgeNumber = 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import BackgroundTasks
|
|||||||
class BGTask {
|
class BGTask {
|
||||||
static let updateDBMissingID = "com.tt.ifeel.dbUpdateMissing"
|
static let updateDBMissingID = "com.tt.ifeel.dbUpdateMissing"
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class func runFillInMissingDatesTask(task: BGProcessingTask) {
|
class func runFillInMissingDatesTask(task: BGProcessingTask) {
|
||||||
BGTask.scheduleBackgroundProcessing()
|
BGTask.scheduleBackgroundProcessing()
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ class BGTask {
|
|||||||
task.setTaskCompleted(success: false)
|
task.setTaskCompleted(success: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
PersistenceController.shared.fillInMissingDates()
|
DataController.shared.fillInMissingDates()
|
||||||
task.setTaskCompleted(success: true)
|
task.setTaskCompleted(success: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>_XCCurrentVersionName</key>
|
|
||||||
<string>Shared 2.xcdatamodel</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19574" systemVersion="21D49" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
|
|
||||||
<entity name="MoodEntry" representedClassName="MoodEntry" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="canDelete" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
|
||||||
<attribute name="canEdit" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
|
||||||
<attribute name="entryType" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
|
||||||
<attribute name="forDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
|
||||||
<attribute name="moodValue" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
|
||||||
<attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
|
||||||
<attribute name="weekDay" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
|
||||||
</entity>
|
|
||||||
<elements>
|
|
||||||
<element name="MoodEntry" positionX="-63" positionY="-18" width="128" height="134"/>
|
|
||||||
</elements>
|
|
||||||
</model>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19574" systemVersion="21C52" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
|
|
||||||
<entity name="MoodEntry" representedClassName="MoodEntry" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="canDelete" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
|
||||||
<attribute name="canEdit" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
|
||||||
<attribute name="forDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
|
||||||
<attribute name="moodValue" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
|
||||||
<attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
|
||||||
<attribute name="weekDay" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
|
||||||
</entity>
|
|
||||||
<elements>
|
|
||||||
<element name="MoodEntry" positionX="-63" positionY="-18" width="128" height="119"/>
|
|
||||||
</elements>
|
|
||||||
</model>
|
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
import BackgroundTasks
|
import BackgroundTasks
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ struct FeelsApp: App {
|
|||||||
@Environment(\.scenePhase) private var scenePhase
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||||
|
|
||||||
let persistenceController = PersistenceController.shared
|
let dataController = DataController.shared
|
||||||
@StateObject var iapManager = IAPManager()
|
@StateObject var iapManager = IAPManager()
|
||||||
@AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults) private var firstLaunchDate = Date()
|
@AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults) private var firstLaunchDate = Date()
|
||||||
@State private var showSubscriptionFromWidget = false
|
@State private var showSubscriptionFromWidget = false
|
||||||
@@ -25,20 +26,16 @@ struct FeelsApp: App {
|
|||||||
BGTask.runFillInMissingDatesTask(task: task as! BGProcessingTask)
|
BGTask.runFillInMissingDatesTask(task: task as! BGProcessingTask)
|
||||||
}
|
}
|
||||||
UIApplication.shared.applicationIconBadgeNumber = 0
|
UIApplication.shared.applicationIconBadgeNumber = 0
|
||||||
// PersistenceController.shared.clearDB()
|
|
||||||
// PersistenceController.shared.populateMemory()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
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)),
|
MainTabView(dayView: DayView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: false)),
|
||||||
monthView: MonthView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: true)),
|
monthView: MonthView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: true)),
|
||||||
yearView: YearView(viewModel: YearViewModel()),
|
yearView: YearView(viewModel: YearViewModel()),
|
||||||
insightsView: InsightsView(),
|
insightsView: InsightsView(),
|
||||||
customizeView: CustomizeView())
|
customizeView: CustomizeView())
|
||||||
.environment(\.managedObjectContext, persistenceController.viewContext)
|
.modelContainer(dataController.container)
|
||||||
.environmentObject(iapManager)
|
.environmentObject(iapManager)
|
||||||
.sheet(isPresented: $showSubscriptionFromWidget) {
|
.sheet(isPresented: $showSubscriptionFromWidget) {
|
||||||
FeelsSubscriptionStoreView()
|
FeelsSubscriptionStoreView()
|
||||||
|
|||||||
@@ -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))!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
81
Shared/Models/MoodEntryModel.swift
Normal file
81
Shared/Models/MoodEntryModel.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,13 +7,14 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class MoodEntryFunctions {
|
class MoodEntryFunctions {
|
||||||
static func padMoodEntriesForCalendar(entries grouped: [Int: [Int: [MoodEntry]]]) -> [Int: [Int: [MoodEntry]]] {
|
static func padMoodEntriesForCalendar(entries grouped: [Int: [Int: [MoodEntryModel]]]) -> [Int: [Int: [MoodEntryModel]]] {
|
||||||
var newGrouped = [Int: [Int: [MoodEntry]]]()
|
var newGrouped = [Int: [Int: [MoodEntryModel]]]()
|
||||||
|
|
||||||
let allYears = grouped.keys.sorted(by: > )
|
let allYears = grouped.keys.sorted(by: > )
|
||||||
for year in allYears {
|
for year in allYears {
|
||||||
var newMonth = [Int: [MoodEntry]]()
|
var newMonth = [Int: [MoodEntryModel]]()
|
||||||
|
|
||||||
let oldMonths = grouped[year]!
|
let oldMonths = grouped[year]!
|
||||||
let monthKeys = oldMonths.keys.sorted(by: > )
|
let monthKeys = oldMonths.keys.sorted(by: > )
|
||||||
@@ -27,12 +28,12 @@ class MoodEntryFunctions {
|
|||||||
return newGrouped
|
return newGrouped
|
||||||
}
|
}
|
||||||
|
|
||||||
static func padMoodEntriesMonth(monthEntries entries: [MoodEntry]) -> [MoodEntry] {
|
static func padMoodEntriesMonth(monthEntries entries: [MoodEntryModel]) -> [MoodEntryModel] {
|
||||||
let sortedEntries = entries.sorted(by: { $0.forDate! < $1.forDate! })
|
let sortedEntries = entries.sorted(by: { $0.forDate < $1.forDate })
|
||||||
var mutableEntries = sortedEntries
|
var mutableEntries = sortedEntries
|
||||||
|
|
||||||
if let firstDate = sortedEntries.first {
|
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
|
// 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
|
// need to add in the missing entries, as placeholders, to the beignning to get
|
||||||
@@ -40,16 +41,16 @@ class MoodEntryFunctions {
|
|||||||
// and entry is on the 13th ... this needs to show on the 13th entry spot
|
// and entry is on the 13th ... this needs to show on the 13th entry spot
|
||||||
var startOfMonth = date.startOfMonth
|
var startOfMonth = date.startOfMonth
|
||||||
startOfMonth = Calendar.current.date(byAdding: .hour, value: 9, to: 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)
|
var missingDates = Date.dates(from: startOfMonth, toDate: lastMissingDate, includingToDate: true)
|
||||||
missingDates = missingDates.dropLast()
|
missingDates = missingDates.dropLast()
|
||||||
|
|
||||||
for date in missingDates {
|
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: {
|
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
|
// fill in calendar day offset .. if month starts on wed we need to
|
||||||
@@ -57,7 +58,7 @@ class MoodEntryFunctions {
|
|||||||
if let firstDate = mutableEntries.first?.forDate {
|
if let firstDate = mutableEntries.first?.forDate {
|
||||||
let weekday = Int16(Calendar.current.component(.weekday, from: firstDate))
|
let weekday = Int16(Calendar.current.component(.weekday, from: firstDate))
|
||||||
for _ in 1..<weekday {
|
for _ in 1..<weekday {
|
||||||
mutableEntries.insert(PersistenceController.shared.generateObjectNotInArray(withMood: .placeholder), at: 0)
|
mutableEntries.insert(DataController.shared.generateObjectNotInArray(withMood: .placeholder), at: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
82
Shared/Persisence/DataController.swift
Normal file
82
Shared/Persisence/DataController.swift
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
//
|
||||||
|
// DataController.swift
|
||||||
|
// Feels
|
||||||
|
//
|
||||||
|
// SwiftData controller replacing Core Data PersistenceController.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftData
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class DataController: ObservableObject {
|
||||||
|
static let shared = DataController()
|
||||||
|
|
||||||
|
private(set) var container: ModelContainer
|
||||||
|
|
||||||
|
var modelContext: ModelContext {
|
||||||
|
container.mainContext
|
||||||
|
}
|
||||||
|
|
||||||
|
private var useCloudKit: Bool {
|
||||||
|
GroupUserDefaults.groupDefaults.bool(forKey: UserDefaultsStore.Keys.useCloudKit.rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listeners for data changes (keeping existing pattern)
|
||||||
|
var switchContainerListeners = [(() -> Void)]()
|
||||||
|
private var editedDataClosure = [() -> Void]()
|
||||||
|
|
||||||
|
// Computed properties for earliest/latest entries
|
||||||
|
var earliestEntry: MoodEntryModel? {
|
||||||
|
var descriptor = FetchDescriptor<MoodEntryModel>(
|
||||||
|
sortBy: [SortDescriptor(\.forDate, order: .forward)]
|
||||||
|
)
|
||||||
|
descriptor.fetchLimit = 1
|
||||||
|
return try? modelContext.fetch(descriptor).first
|
||||||
|
}
|
||||||
|
|
||||||
|
var latestEntry: MoodEntryModel? {
|
||||||
|
var descriptor = FetchDescriptor<MoodEntryModel>(
|
||||||
|
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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
77
Shared/Persisence/DataControllerADD.swift
Normal file
77
Shared/Persisence/DataControllerADD.swift
Normal file
@@ -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<MoodEntryModel>(
|
||||||
|
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<Date> = 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])
|
||||||
|
}
|
||||||
|
}
|
||||||
40
Shared/Persisence/DataControllerDELETE.swift
Normal file
40
Shared/Persisence/DataControllerDELETE.swift
Normal file
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
85
Shared/Persisence/DataControllerGET.swift
Normal file
85
Shared/Persisence/DataControllerGET.swift
Normal file
@@ -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<MoodEntryModel>(
|
||||||
|
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<MoodEntryModel>(
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
93
Shared/Persisence/DataControllerHelper.swift
Normal file
93
Shared/Persisence/DataControllerHelper.swift
Normal file
@@ -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..<count {
|
||||||
|
let date = Calendar.current.date(byAdding: .day, value: -idx, to: Date())!
|
||||||
|
let entry = MoodEntryModel(
|
||||||
|
forDate: date,
|
||||||
|
mood: Mood.allValues.randomElement()!,
|
||||||
|
entryType: .listView
|
||||||
|
)
|
||||||
|
entry.timestamp = date
|
||||||
|
entries.append(entry)
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func populateMemory() {
|
||||||
|
#if DEBUG
|
||||||
|
for idx in 1..<255 {
|
||||||
|
let date = Calendar.current.date(byAdding: .day, value: -idx, to: Date())!
|
||||||
|
var moodValue = Int.random(in: 2...4)
|
||||||
|
if idx % 5 == 0 {
|
||||||
|
moodValue = Int.random(in: 0...4)
|
||||||
|
}
|
||||||
|
|
||||||
|
let entry = MoodEntryModel(
|
||||||
|
forDate: date,
|
||||||
|
mood: Mood(rawValue: moodValue) ?? .average,
|
||||||
|
entryType: .listView
|
||||||
|
)
|
||||||
|
entry.timestamp = date
|
||||||
|
modelContext.insert(entry)
|
||||||
|
}
|
||||||
|
save()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an entry that is NOT inserted into the context - used for UI placeholders
|
||||||
|
func generateObjectNotInArray(forDate date: Date = Date(), withMood mood: Mood = .placeholder) -> 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<MoodEntryModel>(
|
||||||
|
sortBy: [SortDescriptor(\.forDate, order: .forward)]
|
||||||
|
)
|
||||||
|
return (try? modelContext.fetch(descriptor)) ?? []
|
||||||
|
}
|
||||||
|
}
|
||||||
24
Shared/Persisence/DataControllerUPDATE.swift
Normal file
24
Shared/Persisence/DataControllerUPDATE.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<MoodEntry>(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<MoodEntry>(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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<MoodEntry>(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<MoodEntry>(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])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
//
|
|
||||||
// PersistenceDELETE.swift
|
|
||||||
// Feels
|
|
||||||
//
|
|
||||||
// Created by Trey Tartt on 2/17/22.
|
|
||||||
//
|
|
||||||
|
|
||||||
import CoreData
|
|
||||||
|
|
||||||
extension PersistenceController {
|
|
||||||
func clearDB() {
|
|
||||||
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<MoodEntry>(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<MoodEntry>(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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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..<count {
|
|
||||||
let newItem = MoodEntry(context: childContext)
|
|
||||||
newItem.timestamp = Calendar.current.date(byAdding: .day, value: -idx, to: Date())
|
|
||||||
newItem.moodValue = Int16(Mood.allValues.randomElement()!.rawValue)
|
|
||||||
let date = Calendar.current.date(byAdding: .day, value: -idx, to: Date())!
|
|
||||||
newItem.forDate = date
|
|
||||||
newItem.weekDay = Int16(Calendar.current.component(.weekday, from: date))
|
|
||||||
newItem.canEdit = true
|
|
||||||
newItem.canDelete = true
|
|
||||||
entries.append(newItem)
|
|
||||||
}
|
|
||||||
return entries
|
|
||||||
}
|
|
||||||
|
|
||||||
func populateMemory() {
|
|
||||||
#if debug
|
|
||||||
for idx in 1..<255 {
|
|
||||||
let newItem = MoodEntry(context: viewContext)
|
|
||||||
newItem.timestamp = Calendar.current.date(byAdding: .day, value: -idx, to: Date())
|
|
||||||
newItem.moodValue = Int16.random(in: 2 ... 4)
|
|
||||||
if idx % 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))
|
|
||||||
}
|
|
||||||
do {
|
|
||||||
try 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)")
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateObjectNotInArray(forDate date: Date = Date(), withMood mood: Mood = .placeholder) -> 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<MoodEntry>(entityName: "MoodEntry")
|
|
||||||
// fetchRequest.predicate = predicate
|
|
||||||
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)]
|
|
||||||
let data = try! viewContext.fetch(fetchRequest)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
79
Shared/Persisence/SharedModelContainer.swift
Normal file
79
Shared/Persisence/SharedModelContainer.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,21 +13,21 @@ typealias Month = Int
|
|||||||
protocol ChartDataBuildable {
|
protocol ChartDataBuildable {
|
||||||
associatedtype ChartType: ChartViewItemBuildable
|
associatedtype ChartType: ChartViewItemBuildable
|
||||||
// [Year: [Month: [View]]
|
// [Year: [Month: [View]]
|
||||||
func buildGridData(withData data: [MoodEntry]) -> [Year: [Month: [ChartType]]]
|
func buildGridData(withData data: [MoodEntryModel]) -> [Year: [Month: [ChartType]]]
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ChartDataBuildable {
|
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]]]()
|
var returnData = [Int: [Int: [ChartType]]]()
|
||||||
|
|
||||||
if let earliestEntry = data.first,
|
if let earliestEntry = data.first,
|
||||||
let lastEntry = data.last {
|
let lastEntry = data.last {
|
||||||
|
|
||||||
let calendar = Calendar.current
|
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 earliestYear = components.year!
|
||||||
|
|
||||||
let latestComponents = calendar.dateComponents([.year], from: lastEntry.forDate!)
|
let latestComponents = calendar.dateComponents([.year], from: lastEntry.forDate)
|
||||||
let latestYear = latestComponents.year!
|
let latestYear = latestComponents.year!
|
||||||
|
|
||||||
for year in earliestYear...latestYear {
|
for year in earliestYear...latestYear {
|
||||||
@@ -52,7 +52,7 @@ extension ChartDataBuildable {
|
|||||||
|
|
||||||
let items = data.filter({ entry in
|
let items = data.filter({ entry in
|
||||||
let components = calendar.dateComponents([.month, .year], from: startDateOfMonth)
|
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)
|
return (components.month == entryComponents.month && components.year == entryComponents.year)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ extension ChartDataBuildable {
|
|||||||
return returnData
|
return returnData
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createViewFor(monthEntries: [MoodEntry], forMonth month: Date) -> [ChartType] {
|
private func createViewFor(monthEntries: [MoodEntryModel], forMonth month: Date) -> [ChartType] {
|
||||||
var filledOutArray = [ChartType]()
|
var filledOutArray = [ChartType]()
|
||||||
|
|
||||||
let calendar = Calendar.current
|
let calendar = Calendar.current
|
||||||
@@ -73,7 +73,7 @@ extension ChartDataBuildable {
|
|||||||
|
|
||||||
for day in 1...numDays {
|
for day in 1...numDays {
|
||||||
if let item = monthEntries.filter({ entry in
|
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
|
let date = components.day
|
||||||
return day == date
|
return day == date
|
||||||
}).first {
|
}).first {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
struct Constants {
|
struct Constants {
|
||||||
static let groupShareId = "group.com.tt.ifeel"
|
static let groupShareId = "group.com.tt.ifeel"
|
||||||
@@ -70,7 +71,7 @@ class Random {
|
|||||||
return newValue
|
return newValue
|
||||||
}
|
}
|
||||||
|
|
||||||
static func createTotalPerc(fromEntries entries: [MoodEntry]) -> [MoodMetrics] {
|
static func createTotalPerc(fromEntries entries: [MoodEntryModel]) -> [MoodMetrics] {
|
||||||
let filteredEntries = entries.filter({
|
let filteredEntries = entries.filter({
|
||||||
return ![.missing, .placeholder].contains($0.mood)
|
return ![.missing, .placeholder].contains($0.mood)
|
||||||
})
|
})
|
||||||
@@ -78,7 +79,7 @@ class Random {
|
|||||||
|
|
||||||
for (_, mood) in Mood.allValues.enumerated() {
|
for (_, mood) in Mood.allValues.enumerated() {
|
||||||
let moodEntries = filteredEntries.filter({
|
let moodEntries = filteredEntries.filter({
|
||||||
Int($0.moodValue) == mood.rawValue
|
$0.moodValue == mood.rawValue
|
||||||
})
|
})
|
||||||
let total = moodEntries.count
|
let total = moodEntries.count
|
||||||
let perc = (Float(total) / Float(filteredEntries.count)) * 100
|
let perc = (Float(total) / Float(filteredEntries.count)) * 100
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
// Created by Trey Tartt on 2/17/22.
|
// Created by Trey Tartt on 2/17/22.
|
||||||
//
|
//
|
||||||
|
|
||||||
import CoreData
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -29,6 +29,7 @@ import SwiftUI
|
|||||||
db should contain 3/2 | db should contain 3/3
|
db should contain 3/2 | db should contain 3/3
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
@MainActor
|
||||||
class ShowBasedOnVoteLogics {
|
class ShowBasedOnVoteLogics {
|
||||||
static private func returnCurrentVoteStatus(onboardingData: OnboardingData) -> (passedTimeToVote: Bool, inputDay: DayOptions) {
|
static private func returnCurrentVoteStatus(onboardingData: OnboardingData) -> (passedTimeToVote: Bool, inputDay: DayOptions) {
|
||||||
let passedTimeToVote = ShowBasedOnVoteLogics.passedTodaysVotingUnlock(voteDate: onboardingData.date)
|
let passedTimeToVote = ShowBasedOnVoteLogics.passedTodaysVotingUnlock(voteDate: onboardingData.date)
|
||||||
@@ -58,19 +59,12 @@ class ShowBasedOnVoteLogics {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
static public func isMissingCurrentVote(onboardingData: OnboardingData) -> Bool {
|
static public func isMissingCurrentVote(onboardingData: OnboardingData) -> Bool {
|
||||||
let startDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboardingData).startOfDay
|
let startDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: onboardingData).startOfDay
|
||||||
let endDate = startDate.endOfDay
|
let endDate = startDate.endOfDay
|
||||||
|
|
||||||
let fetchRequest = NSFetchRequest<MoodEntry>(entityName: "MoodEntry")
|
let entry = DataController.shared.getEntry(byDate: startDate)
|
||||||
let fromPredicate = NSPredicate(format: "%@ <= %K", startDate
|
return entry == nil || entry?.mood == .missing
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static public func getCurrentVotingDate(onboardingData: OnboardingData) -> Date {
|
static public func getCurrentVotingDate(onboardingData: OnboardingData) -> Date {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class Stats {
|
class Stats {
|
||||||
static func getCountFor(moodType: Mood, inData data: [MoodEntry]) -> Int {
|
static func getCountFor(moodType: Mood, inData data: [MoodEntryModel]) -> Int {
|
||||||
let num = data.filter({
|
let num = data.filter({
|
||||||
$0.moodValue == moodType.rawValue
|
$0.moodValue == moodType.rawValue
|
||||||
}).count
|
}).count
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import CoreData
|
import SwiftData
|
||||||
|
|
||||||
struct AddMoodHeaderView: View {
|
struct AddMoodHeaderView: View {
|
||||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||||
@@ -255,11 +255,11 @@ struct AddMoodHeaderView_Previews: PreviewProvider {
|
|||||||
Group {
|
Group {
|
||||||
AddMoodHeaderView(addItemHeaderClosure: { (_,_) in
|
AddMoodHeaderView(addItemHeaderClosure: { (_,_) in
|
||||||
|
|
||||||
}).environment(\.managedObjectContext, PersistenceController.shared.viewContext)
|
}).modelContainer(DataController.shared.container)
|
||||||
|
|
||||||
AddMoodHeaderView(addItemHeaderClosure: { (_,_) in
|
AddMoodHeaderView(addItemHeaderClosure: { (_,_) in
|
||||||
|
|
||||||
}).preferredColorScheme(.dark).environment(\.managedObjectContext, PersistenceController.shared.viewContext)
|
}).preferredColorScheme(.dark).modelContainer(DataController.shared.container)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
struct BGViewItem: View {
|
struct BGViewItem: View {
|
||||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
@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 {
|
struct BGView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
BGView().environment(\.managedObjectContext, PersistenceController.shared.viewContext)
|
BGView().modelContainer(DataController.shared.container)
|
||||||
.onAppear(perform: {
|
.onAppear(perform: {
|
||||||
PersistenceController.shared.populateMemory()
|
DataController.shared.populateMemory()
|
||||||
})
|
})
|
||||||
|
|
||||||
BGView()
|
BGView()
|
||||||
.preferredColorScheme(.dark)
|
.preferredColorScheme(.dark)
|
||||||
.environment(\.managedObjectContext, PersistenceController.shared.viewContext)
|
.modelContainer(DataController.shared.container)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import CoreData
|
import SwiftData
|
||||||
import Charts
|
import Charts
|
||||||
|
|
||||||
struct DayViewConstants {
|
struct DayViewConstants {
|
||||||
@@ -15,8 +15,6 @@ struct DayViewConstants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct DayView: View {
|
struct DayView: View {
|
||||||
@Environment(\.managedObjectContext) private var viewContext
|
|
||||||
|
|
||||||
@AppStorage(UserDefaultsStore.Keys.deleteEnable.rawValue, store: GroupUserDefaults.groupDefaults) private var deleteEnabled = true
|
@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
|
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||||
@@ -30,7 +28,7 @@ struct DayView: View {
|
|||||||
|
|
||||||
// MARK: edit row properties
|
// MARK: edit row properties
|
||||||
@State private var showingSheet = false
|
@State private var showingSheet = false
|
||||||
@State private var selectedEntry: MoodEntry?
|
@State private var selectedEntry: MoodEntryModel?
|
||||||
//
|
//
|
||||||
|
|
||||||
// MARK: ?? properties
|
// MARK: ?? properties
|
||||||
@@ -100,7 +98,7 @@ struct DayView: View {
|
|||||||
}
|
}
|
||||||
.padding([.leading, .trailing])
|
.padding([.leading, .trailing])
|
||||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
|
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
|
||||||
PersistenceController.shared.fillInMissingDates()
|
DataController.shared.fillInMissingDates()
|
||||||
viewModel.updateData()
|
viewModel.updateData()
|
||||||
}
|
}
|
||||||
.background(
|
.background(
|
||||||
@@ -164,13 +162,13 @@ extension DayView {
|
|||||||
.background(.ultraThinMaterial)
|
.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) {
|
VStack(spacing: 12) {
|
||||||
// for reach all entries
|
// for reach all entries
|
||||||
ForEach(entries.sorted(by: {
|
ForEach(entries.sorted(by: {
|
||||||
return $0.forDate! > $1.forDate!
|
return $0.forDate > $1.forDate
|
||||||
}), id: \.self) { entry in
|
}), id: \.self) { entry in
|
||||||
if filteredDays.currentFilters.contains(Int(entry.weekDay)) {
|
if filteredDays.currentFilters.contains(entry.weekDay) {
|
||||||
EntryListView(entry: entry)
|
EntryListView(entry: entry)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture(perform: {
|
.onTapGesture(perform: {
|
||||||
@@ -195,13 +193,14 @@ struct ViewOffsetKey: PreferenceKey {
|
|||||||
|
|
||||||
struct DayView_Previews: PreviewProvider {
|
struct DayView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
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: {
|
.onAppear(perform: {
|
||||||
PersistenceController.shared.populateMemory()
|
DataController.shared.populateMemory()
|
||||||
})
|
})
|
||||||
|
|
||||||
DayView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: false))
|
DayView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: false))
|
||||||
.preferredColorScheme(.dark)
|
.preferredColorScheme(.dark)
|
||||||
.environment(\.managedObjectContext, PersistenceController.shared.viewContext)
|
.modelContainer(DataController.shared.container)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,11 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import CoreData
|
import SwiftData
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class DayViewViewModel: ObservableObject {
|
class DayViewViewModel: ObservableObject {
|
||||||
@Published var grouped = [Int: [Int: [MoodEntry]]]()
|
@Published var grouped = [Int: [Int: [MoodEntryModel]]]()
|
||||||
@Published var numberOfItems = 0
|
@Published var numberOfItems = 0
|
||||||
|
|
||||||
let addMonthStartWeekdayPadding: Bool
|
let addMonthStartWeekdayPadding: Bool
|
||||||
@@ -28,22 +29,16 @@ class DayViewViewModel: ObservableObject {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
return num
|
return num
|
||||||
|
|
||||||
// grouped.keys.map{
|
|
||||||
// grouped[$0]!.values.reduce(0) { sum, array in
|
|
||||||
// sum + array.count
|
|
||||||
// }
|
|
||||||
// }.reduce(0, +)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init(addMonthStartWeekdayPadding: Bool) {
|
init(addMonthStartWeekdayPadding: Bool) {
|
||||||
self.addMonthStartWeekdayPadding = addMonthStartWeekdayPadding
|
self.addMonthStartWeekdayPadding = addMonthStartWeekdayPadding
|
||||||
|
|
||||||
PersistenceController.shared.switchContainerListeners.append {
|
DataController.shared.switchContainerListeners.append {
|
||||||
self.getGroupedData(addMonthStartWeekdayPadding: self.addMonthStartWeekdayPadding)
|
self.getGroupedData(addMonthStartWeekdayPadding: self.addMonthStartWeekdayPadding)
|
||||||
}
|
}
|
||||||
|
|
||||||
PersistenceController.shared.addNewDataListener {
|
DataController.shared.addNewDataListener {
|
||||||
withAnimation{
|
withAnimation{
|
||||||
self.updateData()
|
self.updateData()
|
||||||
}
|
}
|
||||||
@@ -52,7 +47,7 @@ class DayViewViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func getGroupedData(addMonthStartWeekdayPadding: Bool) {
|
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 {
|
if addMonthStartWeekdayPadding {
|
||||||
newStuff = MoodEntryFunctions.padMoodEntriesForCalendar(entries: newStuff)
|
newStuff = MoodEntryFunctions.padMoodEntriesForCalendar(entries: newStuff)
|
||||||
}
|
}
|
||||||
@@ -65,11 +60,11 @@ class DayViewViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func add(mood: Mood, forDate date: Date, entryType: EntryType) {
|
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) {
|
public func update(entry: MoodEntryModel, toMood mood: Mood) {
|
||||||
if !PersistenceController.shared.update(entryDate: entry.forDate!, withModd: mood) {
|
if !DataController.shared.update(entryDate: entry.forDate, withMood: mood) {
|
||||||
#warning("show error")
|
#warning("show error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,47 +73,37 @@ class DayViewViewModel: ObservableObject {
|
|||||||
if let monthEntries = grouped[year],
|
if let monthEntries = grouped[year],
|
||||||
let entries = monthEntries[month] {
|
let entries = monthEntries[month] {
|
||||||
var mutableEntries = entries.sorted(by: {
|
var mutableEntries = entries.sorted(by: {
|
||||||
$0.forDate! > $1.forDate!
|
$0.forDate > $1.forDate
|
||||||
})
|
})
|
||||||
var entriesToDelete = [MoodEntry]()
|
var entriesToDelete = [MoodEntryModel]()
|
||||||
for idx in offsets {
|
for idx in offsets {
|
||||||
let obj = mutableEntries.remove(at: idx)
|
let obj = mutableEntries.remove(at: idx)
|
||||||
entriesToDelete.append(obj)
|
entriesToDelete.append(obj)
|
||||||
}
|
}
|
||||||
entriesToDelete.forEach({ entry in
|
entriesToDelete.forEach({ entry in
|
||||||
let entryDate = entry.forDate!
|
let entryDate = entry.forDate
|
||||||
PersistenceController.shared.viewContext.delete(entry)
|
DataController.shared.modelContext.delete(entry)
|
||||||
self.add(mood: .missing, forDate: entryDate, entryType: .listView)
|
self.add(mood: .missing, forDate: entryDate, entryType: .listView)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
DataController.shared.save()
|
||||||
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)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func updateTitleHeader(forEntry entry: MoodEntry?) -> String {
|
static func updateTitleHeader(forEntry entry: MoodEntryModel?) -> String {
|
||||||
guard let entry = entry else {
|
guard let entry = entry else {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let forDate = entry.forDate else {
|
let forDate = entry.forDate
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
let components = Calendar.current.dateComponents([.day, .month, .year], from: forDate)
|
let components = Calendar.current.dateComponents([.day, .month, .year], from: forDate)
|
||||||
// let day = components.day!
|
|
||||||
let month = components.month!
|
let month = components.month!
|
||||||
let year = components.year!
|
let year = components.year!
|
||||||
|
|
||||||
let monthName = Random.monthName(fromMonthInt: month)
|
let monthName = Random.monthName(fromMonthInt: month)
|
||||||
let weekday = Random.weekdayName(fromDate:entry.forDate!)
|
let weekday = Random.weekdayName(fromDate: entry.forDate)
|
||||||
let dayz = Random.dayFormat(fromDate:entry.forDate!)
|
let dayz = Random.dayFormat(fromDate: entry.forDate)
|
||||||
|
|
||||||
let string = weekday + " " + monthName + " " + dayz + " " + String(year)
|
let string = weekday + " " + monthName + " " + dayz + " " + String(year)
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ struct EntryListView: View {
|
|||||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
public let entry: MoodEntry
|
public let entry: MoodEntryModel
|
||||||
|
|
||||||
private var moodColor: Color {
|
private var moodColor: Color {
|
||||||
moodTint.color(forMood: entry.mood)
|
moodTint.color(forMood: entry.mood)
|
||||||
@@ -53,14 +53,14 @@ struct EntryListView: View {
|
|||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
// Date row
|
// Date row
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Text(Random.weekdayName(fromDate: entry.forDate!))
|
Text(Random.weekdayName(fromDate: entry.forDate))
|
||||||
.font(.system(size: 17, weight: .semibold))
|
.font(.system(size: 17, weight: .semibold))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
Text("•")
|
Text("•")
|
||||||
.foregroundColor(textColor.opacity(0.4))
|
.foregroundColor(textColor.opacity(0.4))
|
||||||
|
|
||||||
Text(Random.dayFormat(fromDate: entry.forDate!))
|
Text(Random.dayFormat(fromDate: entry.forDate))
|
||||||
.font(.system(size: 17, weight: .medium))
|
.font(.system(size: 17, weight: .medium))
|
||||||
.foregroundColor(textColor.opacity(0.8))
|
.foregroundColor(textColor.opacity(0.8))
|
||||||
}
|
}
|
||||||
@@ -115,7 +115,7 @@ struct EntryListView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct EntryListView_Previews: PreviewProvider {
|
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 {
|
static var previews: some View {
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import CoreData
|
|
||||||
import Charts
|
import Charts
|
||||||
|
|
||||||
struct GraphView: View {
|
struct GraphView: View {
|
||||||
|
|||||||
@@ -23,18 +23,19 @@ struct HeaderPercView: View {
|
|||||||
let backDays: Int
|
let backDays: Int
|
||||||
let type: PercViewType
|
let type: PercViewType
|
||||||
|
|
||||||
|
@MainActor
|
||||||
init(fakeData: Bool, backDays: Int, type: PercViewType) {
|
init(fakeData: Bool, backDays: Int, type: PercViewType) {
|
||||||
self.type = type
|
self.type = type
|
||||||
self.backDays = backDays
|
self.backDays = backDays
|
||||||
var moodEntries: [MoodEntry]?
|
var moodEntries: [MoodEntryModel]?
|
||||||
|
|
||||||
if fakeData {
|
if fakeData {
|
||||||
moodEntries = PersistenceController.shared.randomEntries(count: 10)
|
moodEntries = DataController.shared.randomEntries(count: 10)
|
||||||
} else {
|
} else {
|
||||||
var daysAgo = Calendar.current.date(byAdding: .day, value: -backDays, to: Date())!
|
var daysAgo = Calendar.current.date(byAdding: .day, value: -backDays, to: Date())!
|
||||||
daysAgo = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: daysAgo)!
|
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 {
|
if let moodEntries = moodEntries {
|
||||||
|
|||||||
@@ -25,15 +25,15 @@ struct HeaderStatsView : UIViewRepresentable {
|
|||||||
self.tmpHolderToMakeViewDiffefrent = Color.random()
|
self.tmpHolderToMakeViewDiffefrent = Color.random()
|
||||||
entries = [BarChartDataEntry]()
|
entries = [BarChartDataEntry]()
|
||||||
|
|
||||||
var moodEntries: [MoodEntry]?
|
var moodEntries: [MoodEntryModel]?
|
||||||
|
|
||||||
if fakeData {
|
if fakeData {
|
||||||
moodEntries = PersistenceController.shared.randomEntries(count: 10)
|
moodEntries = DataController.shared.randomEntries(count: 10)
|
||||||
} else {
|
} else {
|
||||||
var daysAgo = Calendar.current.date(byAdding: .day, value: -backDays, to: Date())!
|
var daysAgo = Calendar.current.date(byAdding: .day, value: -backDays, to: Date())!
|
||||||
daysAgo = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: daysAgo)!
|
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 {
|
if let moodEntries = moodEntries {
|
||||||
for (index, mood) in Mood.allValues.enumerated() {
|
for (index, mood) in Mood.allValues.enumerated() {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ struct Insight: Identifiable {
|
|||||||
let mood: Mood?
|
let mood: Mood?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class InsightsViewModel: ObservableObject {
|
class InsightsViewModel: ObservableObject {
|
||||||
@Published var monthInsights: [Insight] = []
|
@Published var monthInsights: [Insight] = []
|
||||||
@Published var yearInsights: [Insight] = []
|
@Published var yearInsights: [Insight] = []
|
||||||
@@ -36,9 +37,9 @@ class InsightsViewModel: ObservableObject {
|
|||||||
let allTimeStart = Date(timeIntervalSince1970: 0)
|
let allTimeStart = Date(timeIntervalSince1970: 0)
|
||||||
|
|
||||||
// Fetch entries for each period
|
// Fetch entries for each period
|
||||||
let monthEntries = PersistenceController.shared.getData(startDate: monthStart, 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 = PersistenceController.shared.getData(startDate: yearStart, 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 = PersistenceController.shared.getData(startDate: allTimeStart, 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
|
// Generate insights for each period
|
||||||
monthInsights = generateRandomInsights(entries: monthEntries, periodName: "this month", count: 5)
|
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)
|
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
|
// Filter out missing/placeholder entries
|
||||||
let validEntries = entries.filter { ![.missing, .placeholder].contains($0.mood) }
|
let validEntries = entries.filter { ![.missing, .placeholder].contains($0.mood) }
|
||||||
|
|
||||||
@@ -85,7 +86,7 @@ class InsightsViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Most Common Mood
|
// MARK: - Most Common Mood
|
||||||
private func generateMostCommonMoodInsights(entries: [MoodEntry], periodName: String) -> [Insight] {
|
private func generateMostCommonMoodInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] {
|
||||||
var insights: [Insight] = []
|
var insights: [Insight] = []
|
||||||
let moodCounts = Dictionary(grouping: entries, by: { $0.mood }).mapValues { $0.count }
|
let moodCounts = Dictionary(grouping: entries, by: { $0.mood }).mapValues { $0.count }
|
||||||
|
|
||||||
@@ -120,7 +121,7 @@ class InsightsViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Least Common Mood
|
// MARK: - Least Common Mood
|
||||||
private func generateLeastCommonMoodInsights(entries: [MoodEntry], periodName: String) -> [Insight] {
|
private func generateLeastCommonMoodInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] {
|
||||||
var insights: [Insight] = []
|
var insights: [Insight] = []
|
||||||
let moodCounts = Dictionary(grouping: entries, by: { $0.mood }).mapValues { $0.count }
|
let moodCounts = Dictionary(grouping: entries, by: { $0.mood }).mapValues { $0.count }
|
||||||
|
|
||||||
@@ -143,7 +144,7 @@ class InsightsViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Best Day of Week
|
// MARK: - Best Day of Week
|
||||||
private func generateBestDayInsights(entries: [MoodEntry], periodName: String) -> [Insight] {
|
private func generateBestDayInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] {
|
||||||
var insights: [Insight] = []
|
var insights: [Insight] = []
|
||||||
let weekdayCounts = Dictionary(grouping: entries, by: { Int($0.weekDay) })
|
let weekdayCounts = Dictionary(grouping: entries, by: { Int($0.weekDay) })
|
||||||
var weekdayScores: [Int: Float] = [:]
|
var weekdayScores: [Int: Float] = [:]
|
||||||
@@ -176,7 +177,7 @@ class InsightsViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Worst Day of Week
|
// MARK: - Worst Day of Week
|
||||||
private func generateWorstDayInsights(entries: [MoodEntry], periodName: String) -> [Insight] {
|
private func generateWorstDayInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] {
|
||||||
var insights: [Insight] = []
|
var insights: [Insight] = []
|
||||||
let weekdayCounts = Dictionary(grouping: entries, by: { Int($0.weekDay) })
|
let weekdayCounts = Dictionary(grouping: entries, by: { Int($0.weekDay) })
|
||||||
var weekdayScores: [Int: Float] = [:]
|
var weekdayScores: [Int: Float] = [:]
|
||||||
@@ -208,7 +209,7 @@ class InsightsViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Streak Insights
|
// MARK: - Streak Insights
|
||||||
private func generateStreakInsights(entries: [MoodEntry], periodName: String) -> [Insight] {
|
private func generateStreakInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] {
|
||||||
var insights: [Insight] = []
|
var insights: [Insight] = []
|
||||||
let streakInfo = calculateStreaks(entries: entries)
|
let streakInfo = calculateStreaks(entries: entries)
|
||||||
|
|
||||||
@@ -271,12 +272,12 @@ class InsightsViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Trend Insights
|
// MARK: - Trend Insights
|
||||||
private func generateTrendInsights(entries: [MoodEntry], periodName: String) -> [Insight] {
|
private func generateTrendInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] {
|
||||||
var insights: [Insight] = []
|
var insights: [Insight] = []
|
||||||
|
|
||||||
guard entries.count >= 4 else { return insights }
|
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 halfCount = sortedEntries.count / 2
|
||||||
|
|
||||||
let firstHalf = Array(sortedEntries.prefix(halfCount))
|
let firstHalf = Array(sortedEntries.prefix(halfCount))
|
||||||
@@ -330,7 +331,7 @@ class InsightsViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Positivity Insights
|
// MARK: - Positivity Insights
|
||||||
private func generatePositivityInsights(entries: [MoodEntry], periodName: String) -> [Insight] {
|
private func generatePositivityInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] {
|
||||||
var insights: [Insight] = []
|
var insights: [Insight] = []
|
||||||
|
|
||||||
let positiveDays = entries.filter { [.great, .good].contains($0.mood) }.count
|
let positiveDays = entries.filter { [.great, .good].contains($0.mood) }.count
|
||||||
@@ -393,7 +394,7 @@ class InsightsViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Weekend vs Weekday
|
// MARK: - Weekend vs Weekday
|
||||||
private func generateWeekendVsWeekdayInsights(entries: [MoodEntry], periodName: String) -> [Insight] {
|
private func generateWeekendVsWeekdayInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] {
|
||||||
var insights: [Insight] = []
|
var insights: [Insight] = []
|
||||||
|
|
||||||
let weekendDays = entries.filter { [1, 7].contains(Int($0.weekDay)) } // Sunday = 1, Saturday = 7
|
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
|
// MARK: - Mood Swing Insights
|
||||||
private func generateMoodSwingInsights(entries: [MoodEntry], periodName: String) -> [Insight] {
|
private func generateMoodSwingInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] {
|
||||||
var insights: [Insight] = []
|
var insights: [Insight] = []
|
||||||
|
|
||||||
guard entries.count >= 3 else { return insights }
|
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
|
var swings = 0
|
||||||
|
|
||||||
for i in 1..<sortedEntries.count {
|
for i in 1..<sortedEntries.count {
|
||||||
@@ -483,7 +484,7 @@ class InsightsViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Consistency Insights
|
// MARK: - Consistency Insights
|
||||||
private func generateConsistencyInsights(entries: [MoodEntry], periodName: String) -> [Insight] {
|
private func generateConsistencyInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] {
|
||||||
var insights: [Insight] = []
|
var insights: [Insight] = []
|
||||||
|
|
||||||
let totalDaysInPeriod = calculateDaysInPeriod(periodName: periodName)
|
let totalDaysInPeriod = calculateDaysInPeriod(periodName: periodName)
|
||||||
@@ -535,7 +536,7 @@ class InsightsViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Milestone Insights
|
// MARK: - Milestone Insights
|
||||||
private func generateMilestoneInsights(entries: [MoodEntry], periodName: String) -> [Insight] {
|
private func generateMilestoneInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] {
|
||||||
var insights: [Insight] = []
|
var insights: [Insight] = []
|
||||||
|
|
||||||
let count = entries.count
|
let count = entries.count
|
||||||
@@ -557,7 +558,7 @@ class InsightsViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Comparative Insights
|
// MARK: - Comparative Insights
|
||||||
private func generateComparativeInsights(entries: [MoodEntry], periodName: String) -> [Insight] {
|
private func generateComparativeInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] {
|
||||||
var insights: [Insight] = []
|
var insights: [Insight] = []
|
||||||
|
|
||||||
let moodCounts = Dictionary(grouping: entries, by: { $0.mood }).mapValues { $0.count }
|
let moodCounts = Dictionary(grouping: entries, by: { $0.mood }).mapValues { $0.count }
|
||||||
@@ -606,7 +607,7 @@ class InsightsViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Pattern Insights
|
// MARK: - Pattern Insights
|
||||||
private func generatePatternInsights(entries: [MoodEntry], periodName: String) -> [Insight] {
|
private func generatePatternInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] {
|
||||||
var insights: [Insight] = []
|
var insights: [Insight] = []
|
||||||
|
|
||||||
// Monday blues check
|
// Monday blues check
|
||||||
@@ -662,7 +663,7 @@ class InsightsViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Fun Fact Insights
|
// MARK: - Fun Fact Insights
|
||||||
private func generateFunFactInsights(entries: [MoodEntry], periodName: String) -> [Insight] {
|
private func generateFunFactInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] {
|
||||||
var insights: [Insight] = []
|
var insights: [Insight] = []
|
||||||
|
|
||||||
let greatDays = entries.filter { $0.mood == .great }.count
|
let greatDays = entries.filter { $0.mood == .great }.count
|
||||||
@@ -688,7 +689,7 @@ class InsightsViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate "great day streak potential"
|
// 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
|
let recentGreat = recentEntries.filter { $0.mood == .great }.count
|
||||||
if recentGreat >= 3 {
|
if recentGreat >= 3 {
|
||||||
insights.append(Insight(
|
insights.append(Insight(
|
||||||
@@ -712,7 +713,7 @@ class InsightsViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Motivational Insights
|
// MARK: - Motivational Insights
|
||||||
private func generateMotivationalInsights(entries: [MoodEntry], periodName: String) -> [Insight] {
|
private func generateMotivationalInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] {
|
||||||
var insights: [Insight] = []
|
var insights: [Insight] = []
|
||||||
|
|
||||||
let avgMood = Float(entries.reduce(0) { $0 + Int($1.moodValue) }) / Float(entries.count)
|
let avgMood = Float(entries.reduce(0) { $0 + Int($1.moodValue) }) / Float(entries.count)
|
||||||
@@ -760,7 +761,7 @@ class InsightsViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Rare Mood Insights
|
// MARK: - Rare Mood Insights
|
||||||
private func generateRareMoodInsights(entries: [MoodEntry], periodName: String) -> [Insight] {
|
private func generateRareMoodInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] {
|
||||||
var insights: [Insight] = []
|
var insights: [Insight] = []
|
||||||
|
|
||||||
let moodCounts = Dictionary(grouping: entries, by: { $0.mood }).mapValues { $0.count }
|
let moodCounts = Dictionary(grouping: entries, by: { $0.mood }).mapValues { $0.count }
|
||||||
@@ -800,13 +801,13 @@ class InsightsViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Recent Insights
|
// MARK: - Recent Insights
|
||||||
private func generateRecentInsights(entries: [MoodEntry], periodName: String) -> [Insight] {
|
private func generateRecentInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] {
|
||||||
var insights: [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 }
|
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 {
|
if daysSinceLastEntry == 0 {
|
||||||
insights.append(Insight(
|
insights.append(Insight(
|
||||||
@@ -858,13 +859,13 @@ class InsightsViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Month of Year Insights
|
// MARK: - Month of Year Insights
|
||||||
private func generateMonthOfYearInsights(entries: [MoodEntry], periodName: String) -> [Insight] {
|
private func generateMonthOfYearInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] {
|
||||||
var insights: [Insight] = []
|
var insights: [Insight] = []
|
||||||
|
|
||||||
guard periodName == "this year" || periodName == "all time" else { return insights }
|
guard periodName == "this year" || periodName == "all time" else { return insights }
|
||||||
|
|
||||||
let monthGroups = Dictionary(grouping: entries) { entry -> Int in
|
let monthGroups = Dictionary(grouping: entries) { entry -> Int in
|
||||||
calendar.component(.month, from: entry.forDate!)
|
calendar.component(.month, from: entry.forDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
var monthScores: [Int: Float] = [:]
|
var monthScores: [Int: Float] = [:]
|
||||||
@@ -899,7 +900,7 @@ class InsightsViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Longest Mood Run
|
// MARK: - Longest Mood Run
|
||||||
private func generateLongestMoodRunInsights(entries: [MoodEntry], periodName: String) -> [Insight] {
|
private func generateLongestMoodRunInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] {
|
||||||
var insights: [Insight] = []
|
var insights: [Insight] = []
|
||||||
|
|
||||||
for mood in [Mood.great, .good, .average, .bad, .horrible] {
|
for mood in [Mood.great, .good, .average, .bad, .horrible] {
|
||||||
@@ -928,7 +929,7 @@ class InsightsViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Average Mood Insights
|
// MARK: - Average Mood Insights
|
||||||
private func generateAverageMoodInsights(entries: [MoodEntry], periodName: String) -> [Insight] {
|
private func generateAverageMoodInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] {
|
||||||
var insights: [Insight] = []
|
var insights: [Insight] = []
|
||||||
|
|
||||||
let avgMood = Float(entries.reduce(0) { $0 + Int($1.moodValue) }) / Float(entries.count)
|
let avgMood = Float(entries.reduce(0) { $0 + Int($1.moodValue) }) / Float(entries.count)
|
||||||
@@ -980,8 +981,8 @@ class InsightsViewModel: ObservableObject {
|
|||||||
|
|
||||||
// MARK: - Helper Functions
|
// MARK: - Helper Functions
|
||||||
|
|
||||||
private func calculateStreaks(entries: [MoodEntry]) -> (currentStreak: Int, longestStreak: Int) {
|
private func calculateStreaks(entries: [MoodEntryModel]) -> (currentStreak: Int, longestStreak: Int) {
|
||||||
let sortedEntries = entries.sorted { $0.forDate! > $1.forDate! }
|
let sortedEntries = entries.sorted { $0.forDate > $1.forDate }
|
||||||
guard !sortedEntries.isEmpty else { return (0, 0) }
|
guard !sortedEntries.isEmpty else { return (0, 0) }
|
||||||
|
|
||||||
var currentStreak = 0
|
var currentStreak = 0
|
||||||
@@ -989,14 +990,14 @@ class InsightsViewModel: ObservableObject {
|
|||||||
var tempStreak = 1
|
var tempStreak = 1
|
||||||
|
|
||||||
let today = calendar.startOfDay(for: Date())
|
let today = calendar.startOfDay(for: Date())
|
||||||
if let mostRecent = sortedEntries.first?.forDate,
|
let mostRecent = sortedEntries.first!.forDate
|
||||||
calendar.isDate(mostRecent, inSameDayAs: today) || calendar.isDate(mostRecent, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) {
|
if calendar.isDate(mostRecent, inSameDayAs: today) || calendar.isDate(mostRecent, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) {
|
||||||
currentStreak = 1
|
currentStreak = 1
|
||||||
var checkDate = calendar.date(byAdding: .day, value: -1, to: mostRecent)!
|
var checkDate = calendar.date(byAdding: .day, value: -1, to: mostRecent)!
|
||||||
|
|
||||||
for entry in sortedEntries.dropFirst() {
|
for entry in sortedEntries.dropFirst() {
|
||||||
if let entryDate = entry.forDate,
|
let entryDate = entry.forDate
|
||||||
calendar.isDate(entryDate, inSameDayAs: checkDate) {
|
if calendar.isDate(entryDate, inSameDayAs: checkDate) {
|
||||||
currentStreak += 1
|
currentStreak += 1
|
||||||
checkDate = calendar.date(byAdding: .day, value: -1, to: checkDate)!
|
checkDate = calendar.date(byAdding: .day, value: -1, to: checkDate)!
|
||||||
} else {
|
} else {
|
||||||
@@ -1006,15 +1007,14 @@ class InsightsViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i in 1..<sortedEntries.count {
|
for i in 1..<sortedEntries.count {
|
||||||
if let currentDate = sortedEntries[i].forDate,
|
let currentDate = sortedEntries[i].forDate
|
||||||
let previousDate = sortedEntries[i-1].forDate {
|
let previousDate = sortedEntries[i-1].forDate
|
||||||
let dayDiff = calendar.dateComponents([.day], from: currentDate, to: previousDate).day ?? 0
|
let dayDiff = calendar.dateComponents([.day], from: currentDate, to: previousDate).day ?? 0
|
||||||
if dayDiff == 1 {
|
if dayDiff == 1 {
|
||||||
tempStreak += 1
|
tempStreak += 1
|
||||||
} else {
|
} else {
|
||||||
longestStreak = max(longestStreak, tempStreak)
|
longestStreak = max(longestStreak, tempStreak)
|
||||||
tempStreak = 1
|
tempStreak = 1
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
longestStreak = max(longestStreak, tempStreak)
|
longestStreak = max(longestStreak, tempStreak)
|
||||||
@@ -1022,8 +1022,8 @@ class InsightsViewModel: ObservableObject {
|
|||||||
return (currentStreak, longestStreak)
|
return (currentStreak, longestStreak)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func calculateMoodStreaks(entries: [MoodEntry], moods: [Mood]) -> (current: Int, longest: Int) {
|
private func calculateMoodStreaks(entries: [MoodEntryModel], moods: [Mood]) -> (current: Int, longest: Int) {
|
||||||
let sortedEntries = entries.sorted { $0.forDate! < $1.forDate! }
|
let sortedEntries = entries.sorted { $0.forDate < $1.forDate }
|
||||||
guard !sortedEntries.isEmpty else { return (0, 0) }
|
guard !sortedEntries.isEmpty else { return (0, 0) }
|
||||||
|
|
||||||
var currentStreak = 0
|
var currentStreak = 0
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ struct MonthDetailView: View {
|
|||||||
@StateObject private var shareImage = StupidAssShareObservableObject()
|
@StateObject private var shareImage = StupidAssShareObservableObject()
|
||||||
|
|
||||||
@State private var showingSheet = false
|
@State private var showingSheet = false
|
||||||
@State private var selectedEntry: MoodEntry?
|
@State private var selectedEntry: MoodEntryModel?
|
||||||
@State private var showingUpdateEntryAlert = false
|
@State private var showingUpdateEntryAlert = false
|
||||||
@State private var showUpdateEntryAlert = false
|
@State private var showUpdateEntryAlert = false
|
||||||
|
|
||||||
let monthInt: Int
|
let monthInt: Int
|
||||||
let yearInt: Int
|
let yearInt: Int
|
||||||
@State var entries: [MoodEntry]
|
@State var entries: [MoodEntryModel]
|
||||||
var parentViewModel: DayViewViewModel
|
var parentViewModel: DayViewViewModel
|
||||||
|
|
||||||
let columns = [
|
let columns = [
|
||||||
@@ -90,7 +90,7 @@ struct MonthDetailView: View {
|
|||||||
ForEach(Mood.allValues) { mood in
|
ForEach(Mood.allValues) { mood in
|
||||||
Button(mood.strValue, action: {
|
Button(mood.strValue, action: {
|
||||||
if let selectedEntry = selectedEntry {
|
if let selectedEntry = selectedEntry {
|
||||||
PersistenceController.shared.update(entryDate: selectedEntry.forDate!, withModd: mood)
|
DataController.shared.update(entryDate: selectedEntry.forDate, withMood: mood)
|
||||||
}
|
}
|
||||||
updateEntries()
|
updateEntries()
|
||||||
showUpdateEntryAlert = false
|
showUpdateEntryAlert = false
|
||||||
@@ -102,7 +102,7 @@ struct MonthDetailView: View {
|
|||||||
deleteEnabled,
|
deleteEnabled,
|
||||||
selectedEntry.mood != .missing {
|
selectedEntry.mood != .missing {
|
||||||
Button(String(localized: "content_view_delete_entry"), action: {
|
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()
|
updateEntries()
|
||||||
showUpdateEntryAlert = false
|
showUpdateEntryAlert = false
|
||||||
})
|
})
|
||||||
@@ -161,7 +161,7 @@ struct MonthDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func listViewEntry(forEntry entry: MoodEntry) -> some View {
|
private func listViewEntry(forEntry entry: MoodEntryModel) -> some View {
|
||||||
VStack {
|
VStack {
|
||||||
if entry.mood == .placeholder {
|
if entry.mood == .placeholder {
|
||||||
Text(" ")
|
Text(" ")
|
||||||
@@ -176,7 +176,7 @@ struct MonthDetailView: View {
|
|||||||
alignment: .center)
|
alignment: .center)
|
||||||
.foregroundColor(moodTint.color(forMood: entry.mood))
|
.foregroundColor(moodTint.color(forMood: entry.mood))
|
||||||
} else {
|
} else {
|
||||||
Text(entry.forDate!,
|
Text(entry.forDate,
|
||||||
format: Date.FormatStyle().day())
|
format: Date.FormatStyle().day())
|
||||||
.font(.title3)
|
.font(.title3)
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
@@ -197,7 +197,7 @@ struct MonthDetailView: View {
|
|||||||
private func updateEntries() {
|
private func updateEntries() {
|
||||||
parentViewModel.updateData()
|
parentViewModel.updateData()
|
||||||
let (startDate, endDate) = Date.dateRange(monthInt: monthInt, yearInt: yearInt)
|
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)
|
let padded = MoodEntryFunctions.padMoodEntriesMonth(monthEntries: updatedEntries)
|
||||||
entries = padded
|
entries = padded
|
||||||
}
|
}
|
||||||
@@ -221,8 +221,8 @@ struct MonthDetailView: View {
|
|||||||
struct MonthDetailView_Previews: PreviewProvider {
|
struct MonthDetailView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
MonthDetailView(monthInt: 5, yearInt: 2022, entries:
|
MonthDetailView(monthInt: 5, yearInt: 2022, entries:
|
||||||
PersistenceController.shared.randomEntries(count: 30).sorted(by: {
|
DataController.shared.randomEntries(count: 30).sorted(by: {
|
||||||
$0.forDate! < $1.forDate!
|
$0.forDate < $1.forDate
|
||||||
}), parentViewModel: DayViewViewModel(addMonthStartWeekdayPadding: true))
|
}), parentViewModel: DayViewViewModel(addMonthStartWeekdayPadding: true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ struct MonthView: View {
|
|||||||
struct MonthCard: View {
|
struct MonthCard: View {
|
||||||
let month: Int
|
let month: Int
|
||||||
let year: Int
|
let year: Int
|
||||||
let entries: [MoodEntry]
|
let entries: [MoodEntryModel]
|
||||||
let moodTint: MoodTints
|
let moodTint: MoodTints
|
||||||
let imagePack: MoodImages
|
let imagePack: MoodImages
|
||||||
let textColor: Color
|
let textColor: Color
|
||||||
@@ -177,7 +177,7 @@ struct MonthCard: View {
|
|||||||
|
|
||||||
private var metrics: [MoodMetrics] {
|
private var metrics: [MoodMetrics] {
|
||||||
let (startDate, endDate) = Date.dateRange(monthInt: month, yearInt: year)
|
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)
|
return Random.createTotalPerc(fromEntries: monthEntries)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +250,7 @@ struct MonthCard: View {
|
|||||||
|
|
||||||
// MARK: - Heatmap Cell
|
// MARK: - Heatmap Cell
|
||||||
struct HeatmapCell: View {
|
struct HeatmapCell: View {
|
||||||
let entry: MoodEntry
|
let entry: MoodEntryModel
|
||||||
let moodTint: MoodTints
|
let moodTint: MoodTints
|
||||||
let isFiltered: Bool
|
let isFiltered: Bool
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SampleEntryView: View {
|
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.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ struct SampleEntryView: View {
|
|||||||
.frame(width: 20, height: 20, alignment: .trailing)
|
.frame(width: 20, height: 20, alignment: .trailing)
|
||||||
.foregroundColor(Color(UIColor.systemGray))
|
.foregroundColor(Color(UIColor.systemGray))
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
sampleListEntry = PersistenceController.shared.generateObjectNotInArray(forDate: Date(), withMood: sampleListEntry.mood.next)
|
sampleListEntry = DataController.shared.generateObjectNotInArray(forDate: Date(), withMood: sampleListEntry.mood.next)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|||||||
@@ -119,20 +119,14 @@ struct SettingsView: View {
|
|||||||
if columns.count != 7 {
|
if columns.count != 7 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
let moodEntry = MoodEntry(context: PersistenceController.shared.viewContext)
|
let forDate = dateFormatter.date(from: columns[3])!
|
||||||
moodEntry.canDelete = Bool(columns[0])!
|
let moodValue = Int(columns[4])!
|
||||||
moodEntry.canEdit = Bool(columns[1])!
|
let mood = Mood(rawValue: moodValue) ?? .missing
|
||||||
moodEntry.entryType = Int16(columns[2])!
|
let entryType = EntryType(rawValue: Int(columns[2])!) ?? .listView
|
||||||
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])!
|
DataController.shared.add(mood: mood, forDate: forDate, entryType: entryType)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
PersistenceController.shared.saveAndRunDataListerners()
|
DataController.shared.saveAndRunDataListeners()
|
||||||
EventLogger.log(event: "import_file")
|
EventLogger.log(event: "import_file")
|
||||||
} else {
|
} else {
|
||||||
EventLogger.log(event: "error_import_file")
|
EventLogger.log(event: "error_import_file")
|
||||||
@@ -201,7 +195,7 @@ struct SettingsView: View {
|
|||||||
ZStack {
|
ZStack {
|
||||||
theme.currentTheme.secondaryBGColor
|
theme.currentTheme.secondaryBGColor
|
||||||
Button(action: {
|
Button(action: {
|
||||||
PersistenceController.shared.populateTestData()
|
DataController.shared.populateTestData()
|
||||||
}, label: {
|
}, label: {
|
||||||
Text("Add test data")
|
Text("Add test data")
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
@@ -257,7 +251,7 @@ struct SettingsView: View {
|
|||||||
ZStack {
|
ZStack {
|
||||||
theme.currentTheme.secondaryBGColor
|
theme.currentTheme.secondaryBGColor
|
||||||
Button(action: {
|
Button(action: {
|
||||||
PersistenceController.shared.clearDB()
|
DataController.shared.clearDB()
|
||||||
}, label: {
|
}, label: {
|
||||||
Text("Clear DB")
|
Text("Clear DB")
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
@@ -272,7 +266,7 @@ struct SettingsView: View {
|
|||||||
ZStack {
|
ZStack {
|
||||||
theme.currentTheme.secondaryBGColor
|
theme.currentTheme.secondaryBGColor
|
||||||
Button(action: {
|
Button(action: {
|
||||||
PersistenceController.shared.fixWrongWeekdays()
|
DataController.shared.fixWrongWeekdays()
|
||||||
}, label: {
|
}, label: {
|
||||||
Text("Fix Weekday")
|
Text("Fix Weekday")
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
@@ -581,23 +575,23 @@ struct TextFile: FileDocument {
|
|||||||
var text = ""
|
var text = ""
|
||||||
|
|
||||||
// a simple initializer that creates new, empty documents
|
// a simple initializer that creates new, empty documents
|
||||||
|
@MainActor
|
||||||
init() {
|
init() {
|
||||||
let entries = PersistenceController.shared.getData(startDate: Date(timeIntervalSince1970: 0),
|
let entries = DataController.shared.getData(startDate: Date(timeIntervalSince1970: 0),
|
||||||
endDate: Date(),
|
endDate: Date(),
|
||||||
includedDays: [])
|
includedDays: [])
|
||||||
|
|
||||||
var csvString = "canDelete,canEdit,entryType,forDate,moodValue,timestamp,weekDay\n"
|
var csvString = "canDelete,canEdit,entryType,forDate,moodValue,timestamp,weekDay\n"
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
let canDelete = entry.canDelete
|
let canDelete = entry.canDelete
|
||||||
let canEdit = entry.canEdit
|
let canEdit = entry.canEdit
|
||||||
let entryType = entry.entryType
|
let entryType = entry.entryType
|
||||||
let forDate = entry.forDate!
|
let forDate = entry.forDate
|
||||||
let moodValue = entry.moodValue
|
let moodValue = entry.moodValue
|
||||||
let timestamp = entry.timestamp!
|
let timestamp = entry.timestamp
|
||||||
let weekDay = entry.weekDay
|
let weekDay = entry.weekDay
|
||||||
|
|
||||||
let dataString = "\(canDelete),\(canEdit),\(entryType),\(String(describing: forDate)),\(moodValue),\(String(describing:timestamp)),\(weekDay)\n"
|
let dataString = "\(canDelete),\(canEdit),\(entryType),\(String(describing: forDate)),\(moodValue),\(String(describing:timestamp)),\(weekDay)\n"
|
||||||
// print("DATA: \(dataString)")
|
|
||||||
csvString = csvString.appending(dataString)
|
csvString = csvString.appending(dataString)
|
||||||
}
|
}
|
||||||
text = csvString
|
text = csvString
|
||||||
|
|||||||
@@ -34,16 +34,17 @@ struct SharingListView: View {
|
|||||||
@StateObject private var selectedShare = StupidAssObservableObject()
|
@StateObject private var selectedShare = StupidAssObservableObject()
|
||||||
var sharebleItems = [WrappedSharable]()
|
var sharebleItems = [WrappedSharable]()
|
||||||
|
|
||||||
|
@MainActor
|
||||||
init() {
|
init() {
|
||||||
self.sharebleItems = [
|
self.sharebleItems = [
|
||||||
WrappedSharable(preview: AnyView(
|
WrappedSharable(preview: AnyView(
|
||||||
AllMoodsTotalTemplate(isPreview: true,
|
AllMoodsTotalTemplate(isPreview: true,
|
||||||
startDate: PersistenceController.shared.earliestEntry?.forDate ?? Date(),
|
startDate: DataController.shared.earliestEntry?.forDate ?? Date(),
|
||||||
endDate: Date(),
|
endDate: Date(),
|
||||||
fakeData: false)
|
fakeData: false)
|
||||||
),destination: AnyView(
|
),destination: AnyView(
|
||||||
AllMoodsTotalTemplate(isPreview: false,
|
AllMoodsTotalTemplate(isPreview: false,
|
||||||
startDate: PersistenceController.shared.earliestEntry?.forDate ?? Date(),
|
startDate: DataController.shared.earliestEntry?.forDate ?? Date(),
|
||||||
endDate: Date(),
|
endDate: Date(),
|
||||||
fakeData: false)
|
fakeData: false)
|
||||||
),description: AllMoodsTotalTemplate.description),
|
),description: AllMoodsTotalTemplate.description),
|
||||||
@@ -62,12 +63,12 @@ struct SharingListView: View {
|
|||||||
//////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////
|
||||||
WrappedSharable(preview: AnyView(
|
WrappedSharable(preview: AnyView(
|
||||||
LongestStreakTemplate(isPreview: true,
|
LongestStreakTemplate(isPreview: true,
|
||||||
startDate: PersistenceController.shared.earliestEntry?.forDate ?? Date(),
|
startDate: DataController.shared.earliestEntry?.forDate ?? Date(),
|
||||||
endDate: Date(),
|
endDate: Date(),
|
||||||
fakeData: false)
|
fakeData: false)
|
||||||
), destination: AnyView(
|
), destination: AnyView(
|
||||||
LongestStreakTemplate(isPreview: false,
|
LongestStreakTemplate(isPreview: false,
|
||||||
startDate: PersistenceController.shared.earliestEntry?.forDate ?? Date(),
|
startDate: DataController.shared.earliestEntry?.forDate ?? Date(),
|
||||||
endDate: Date(),
|
endDate: Date(),
|
||||||
fakeData: false)
|
fakeData: false)
|
||||||
), description: LongestStreakTemplate.description),
|
), description: LongestStreakTemplate.description),
|
||||||
|
|||||||
@@ -24,20 +24,21 @@ struct AllMoodsTotalTemplate: View, SharingTemplate {
|
|||||||
|
|
||||||
@StateObject private var shareImage = StupidAssShareObservableObject()
|
@StateObject private var shareImage = StupidAssShareObservableObject()
|
||||||
private var entries = [MoodMetrics]()
|
private var entries = [MoodMetrics]()
|
||||||
|
|
||||||
|
@MainActor
|
||||||
init(isPreview: Bool, startDate: Date, endDate: Date, fakeData: Bool) {
|
init(isPreview: Bool, startDate: Date, endDate: Date, fakeData: Bool) {
|
||||||
self.isPreview = isPreview
|
self.isPreview = isPreview
|
||||||
self.startDate = startDate
|
self.startDate = startDate
|
||||||
self.endDate = endDate
|
self.endDate = endDate
|
||||||
|
|
||||||
var moodEntries: [MoodEntry]?
|
var moodEntries: [MoodEntryModel]?
|
||||||
|
|
||||||
if fakeData {
|
if fakeData {
|
||||||
moodEntries = PersistenceController.shared.randomEntries(count: 10)
|
moodEntries = DataController.shared.randomEntries(count: 10)
|
||||||
} else {
|
} else {
|
||||||
|
moodEntries = DataController.shared.getData(startDate:startDate,
|
||||||
moodEntries = PersistenceController.shared.getData(startDate:startDate,
|
endDate: endDate,
|
||||||
endDate: endDate,
|
includedDays: [1,2,3,4,5,6,7])
|
||||||
includedDays: [1,2,3,4,5,6,7])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
totalEntryCount = moodEntries?.count ?? 0
|
totalEntryCount = moodEntries?.count ?? 0
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ struct CurrentStreakTemplate: View, SharingTemplate {
|
|||||||
var isPreview: Bool
|
var isPreview: Bool
|
||||||
var startDate: Date
|
var startDate: Date
|
||||||
var endDate: Date
|
var endDate: Date
|
||||||
let moodEntries: [MoodEntry]
|
let moodEntries: [MoodEntryModel]
|
||||||
|
|
||||||
@State var showSharingTemplate = false
|
@State var showSharingTemplate = false
|
||||||
@StateObject private var shareImage = StupidAssShareObservableObject()
|
@StateObject private var shareImage = StupidAssShareObservableObject()
|
||||||
@@ -33,23 +33,23 @@ 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) {
|
init(isPreview: Bool, startDate: Date, endDate: Date, fakeData: Bool) {
|
||||||
self.isPreview = isPreview
|
self.isPreview = isPreview
|
||||||
self.startDate = startDate
|
self.startDate = startDate
|
||||||
self.endDate = endDate
|
self.endDate = endDate
|
||||||
|
|
||||||
var _moodEntries: [MoodEntry]?
|
var _moodEntries: [MoodEntryModel]?
|
||||||
|
|
||||||
if fakeData {
|
if fakeData {
|
||||||
_moodEntries = PersistenceController.shared.randomEntries(count: 10)
|
_moodEntries = DataController.shared.randomEntries(count: 10)
|
||||||
} else {
|
} else {
|
||||||
|
_moodEntries = DataController.shared.getData(startDate:startDate,
|
||||||
_moodEntries = PersistenceController.shared.getData(startDate:startDate,
|
endDate: endDate,
|
||||||
endDate: endDate,
|
includedDays: [1,2,3,4,5,6,7])
|
||||||
includedDays: [1,2,3,4,5,6,7])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.moodEntries = _moodEntries ?? [MoodEntry]()
|
self.moodEntries = _moodEntries ?? [MoodEntryModel]()
|
||||||
}
|
}
|
||||||
|
|
||||||
var image: UIImage {
|
var image: UIImage {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ struct LongestStreakTemplate: View, SharingTemplate {
|
|||||||
var endDate: Date
|
var endDate: Date
|
||||||
var fakeData: Bool
|
var fakeData: Bool
|
||||||
|
|
||||||
@State var moodEntries = [MoodEntry]()
|
@State var moodEntries = [MoodEntryModel]()
|
||||||
@State var selectedMood: Mood = .great
|
@State var selectedMood: Mood = .great
|
||||||
|
|
||||||
@State var showSharingTemplate = false
|
@State var showSharingTemplate = false
|
||||||
@@ -57,28 +57,29 @@ struct LongestStreakTemplate: View, SharingTemplate {
|
|||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
private func configureData(fakeData: Bool, mood: Mood) {
|
private func configureData(fakeData: Bool, mood: Mood) {
|
||||||
var _moodEntries: [MoodEntry]?
|
var _moodEntries: [MoodEntryModel]?
|
||||||
|
|
||||||
if fakeData {
|
if fakeData {
|
||||||
_moodEntries = PersistenceController.shared.randomEntries(count: 10)
|
_moodEntries = DataController.shared.randomEntries(count: 10)
|
||||||
} else {
|
} else {
|
||||||
_moodEntries = PersistenceController.shared.getData(startDate:startDate,
|
_moodEntries = DataController.shared.getData(startDate:startDate,
|
||||||
endDate: endDate,
|
endDate: endDate,
|
||||||
includedDays: [1,2,3,4,5,6,7])
|
includedDays: [1,2,3,4,5,6,7])
|
||||||
}
|
}
|
||||||
let data = _moodEntries ?? [MoodEntry]()
|
let data = _moodEntries ?? [MoodEntryModel]()
|
||||||
|
|
||||||
var splitArrays = createSubArrays(fromMoodEntries: data, splitOn: mood)
|
var splitArrays = createSubArrays(fromMoodEntries: data, splitOn: mood)
|
||||||
splitArrays = splitArrays.sorted(by: {
|
splitArrays = splitArrays.sorted(by: {
|
||||||
$0.count > $1.count
|
$0.count > $1.count
|
||||||
} )
|
} )
|
||||||
self.moodEntries = splitArrays.first ?? [MoodEntry]()
|
self.moodEntries = splitArrays.first ?? [MoodEntryModel]()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createSubArrays(fromMoodEntries: [MoodEntry], splitOn: Mood) -> [[MoodEntry]] {
|
private func createSubArrays(fromMoodEntries: [MoodEntryModel], splitOn: Mood) -> [[MoodEntryModel]] {
|
||||||
var splitArrays = [[MoodEntry]]()
|
var splitArrays = [[MoodEntryModel]]()
|
||||||
var currentSplit = [MoodEntry]()
|
var currentSplit = [MoodEntryModel]()
|
||||||
for entry in fromMoodEntries {
|
for entry in fromMoodEntries {
|
||||||
if entry.mood == splitOn {
|
if entry.mood == splitOn {
|
||||||
currentSplit.append(entry)
|
currentSplit.append(entry)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ struct MonthTotalTemplate: View, SharingTemplate {
|
|||||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = .white
|
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = .white
|
||||||
|
|
||||||
private var moodMetrics = [MoodMetrics]()
|
private var moodMetrics = [MoodMetrics]()
|
||||||
private var moodEntries = [MoodEntry]()
|
private var moodEntries = [MoodEntryModel]()
|
||||||
|
|
||||||
let columns = [
|
let columns = [
|
||||||
GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center),
|
GridItem(.flexible(minimum: 5, maximum: .infinity), alignment: .center),
|
||||||
@@ -40,28 +40,23 @@ 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) {
|
init(isPreview: Bool, startDate: Date, endDate: Date, fakeData: Bool) {
|
||||||
self.isPreview = isPreview
|
self.isPreview = isPreview
|
||||||
self.startDate = startDate
|
self.startDate = startDate
|
||||||
self.endDate = endDate
|
self.endDate = endDate
|
||||||
|
|
||||||
var _moodEntries: [MoodEntry]?
|
var _moodEntries: [MoodEntryModel]?
|
||||||
|
|
||||||
if fakeData {
|
if fakeData {
|
||||||
_moodEntries = PersistenceController.shared.randomEntries(count: 10)
|
_moodEntries = DataController.shared.randomEntries(count: 10)
|
||||||
} else {
|
} else {
|
||||||
|
_moodEntries = DataController.shared.getData(startDate: startDate,
|
||||||
_moodEntries = PersistenceController.shared.getData(startDate: startDate,
|
endDate: endDate,
|
||||||
endDate: endDate,
|
includedDays: [1,2,3,4,5,6,7])
|
||||||
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 = _moodEntries ?? [MoodEntry]()
|
moodEntries = _moodEntries ?? [MoodEntryModel]()
|
||||||
|
|
||||||
totalEntryCount = moodEntries.count
|
totalEntryCount = moodEntries.count
|
||||||
moodMetrics = Random.createTotalPerc(fromEntries: moodEntries)
|
moodMetrics = Random.createTotalPerc(fromEntries: moodEntries)
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ struct SmallRollUpHeaderView: View {
|
|||||||
|
|
||||||
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||||
|
|
||||||
let entries: [MoodEntry]
|
let entries: [MoodEntryModel]
|
||||||
private var moodMetrics = [MoodMetrics]()
|
private var moodMetrics = [MoodMetrics]()
|
||||||
|
|
||||||
init(entries: [MoodEntry], viewType: Binding<MainSwitchableViewType>) {
|
init(entries: [MoodEntryModel], viewType: Binding<MainSwitchableViewType>) {
|
||||||
self.entries = entries
|
self.entries = entries
|
||||||
self._viewType = viewType
|
self._viewType = viewType
|
||||||
|
|
||||||
@@ -85,14 +85,14 @@ struct SmallRollUpHeaderView: View {
|
|||||||
struct SmallHeaderView_Previews: PreviewProvider {
|
struct SmallHeaderView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
Group {
|
Group {
|
||||||
SmallRollUpHeaderView(entries: PersistenceController.shared.randomEntries(count: 10),
|
SmallRollUpHeaderView(entries: DataController.shared.randomEntries(count: 10),
|
||||||
viewType: .constant(.total))
|
viewType: .constant(.total))
|
||||||
|
|
||||||
SmallRollUpHeaderView(entries: PersistenceController.shared.randomEntries(count: 10),
|
SmallRollUpHeaderView(entries: DataController.shared.randomEntries(count: 10),
|
||||||
viewType: .constant(.percentageShape))
|
viewType: .constant(.percentageShape))
|
||||||
.background(.gray)
|
.background(.gray)
|
||||||
|
|
||||||
SmallRollUpHeaderView(entries: PersistenceController.shared.randomEntries(count: 10),
|
SmallRollUpHeaderView(entries: DataController.shared.randomEntries(count: 10),
|
||||||
viewType: .constant(.percentage))
|
viewType: .constant(.percentage))
|
||||||
|
|
||||||
.background(.gray)
|
.background(.gray)
|
||||||
|
|||||||
@@ -6,17 +6,15 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import CoreData
|
import SwiftData
|
||||||
|
|
||||||
struct YearView: View {
|
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")]
|
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
|
@State private var toggle = true
|
||||||
|
|
||||||
@FetchRequest(
|
@Query(sort: \MoodEntryModel.forDate, order: .reverse)
|
||||||
sortDescriptors: [NSSortDescriptor(keyPath: \MoodEntry.forDate, ascending: false)],
|
private var items: [MoodEntryModel]
|
||||||
animation: .spring())
|
|
||||||
private var items: FetchedResults<MoodEntry>
|
|
||||||
|
|
||||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
@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
|
@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 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 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 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))!
|
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] {
|
private var metrics: [MoodMetrics] {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class YearViewModel: ObservableObject {
|
class YearViewModel: ObservableObject {
|
||||||
@Published public var entryStartDate: Date = Date()
|
@Published public var entryStartDate: Date = Date()
|
||||||
@Published public var entryEndDate: Date = Date()
|
@Published public var entryEndDate: Date = Date()
|
||||||
@@ -15,7 +16,7 @@ class YearViewModel: ObservableObject {
|
|||||||
// year, month, items
|
// year, month, items
|
||||||
@Published public private(set) var data = [Int: [Int: [DayChartView]]]()
|
@Published public private(set) var data = [Int: [Int: [DayChartView]]]()
|
||||||
@Published public private(set) var numberOfRatings: Int = 0
|
@Published public private(set) var numberOfRatings: Int = 0
|
||||||
public private(set) var uncategorizedData = [MoodEntry]() {
|
public private(set) var uncategorizedData = [MoodEntryModel]() {
|
||||||
didSet {
|
didSet {
|
||||||
self.numberOfRatings = uncategorizedData.count
|
self.numberOfRatings = uncategorizedData.count
|
||||||
}
|
}
|
||||||
@@ -26,12 +27,12 @@ class YearViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateData() {
|
private func updateData() {
|
||||||
let filteredEntries = PersistenceController.shared.getData(startDate: Date(timeIntervalSince1970: 0),
|
let filteredEntries = DataController.shared.getData(startDate: Date(timeIntervalSince1970: 0),
|
||||||
endDate: Date(),
|
endDate: Date(),
|
||||||
includedDays: selectedDays)
|
includedDays: selectedDays)
|
||||||
|
|
||||||
if let fuckingDAte = filteredEntries.sorted(by: { $0.forDate! < $1.forDate! }).first?.forDate {
|
if let firstDate = filteredEntries.sorted(by: { $0.forDate < $1.forDate }).first?.forDate {
|
||||||
self.entryStartDate = fuckingDAte
|
self.entryStartDate = firstDate
|
||||||
}
|
}
|
||||||
self.entryEndDate = Date()
|
self.entryEndDate = Date()
|
||||||
}
|
}
|
||||||
@@ -39,9 +40,9 @@ class YearViewModel: ObservableObject {
|
|||||||
private let chartViewBuilder = DayChartViewChartBuilder()
|
private let chartViewBuilder = DayChartViewChartBuilder()
|
||||||
|
|
||||||
public func filterEntries(startDate: Date, endDate: Date) {
|
public func filterEntries(startDate: Date, endDate: Date) {
|
||||||
let filteredEntries = PersistenceController.shared.getData(startDate: startDate,
|
let filteredEntries = DataController.shared.getData(startDate: startDate,
|
||||||
endDate: endDate,
|
endDate: endDate,
|
||||||
includedDays: selectedDays)
|
includedDays: selectedDays)
|
||||||
data.removeAll()
|
data.removeAll()
|
||||||
let filledOutData = chartViewBuilder.buildGridData(withData: filteredEntries)
|
let filledOutData = chartViewBuilder.buildGridData(withData: filteredEntries)
|
||||||
data = filledOutData
|
data = filledOutData
|
||||||
|
|||||||
Reference in New Issue
Block a user