made bgview equatable so it doesn't get redrawn each time a sheet is shown add more string to localization fill in missing data on launch ... incase they have bgfetch turned off
270 lines
12 KiB
Swift
270 lines
12 KiB
Swift
//
|
|
// Persistence.swift
|
|
// Shared
|
|
//
|
|
// Created by Trey Tartt on 1/5/22.
|
|
//
|
|
|
|
import CoreData
|
|
import WidgetKit
|
|
import Foundation
|
|
import UIKit
|
|
|
|
class PersistenceController {
|
|
private var dataUpdateCall: (() -> Void)?
|
|
private let callDelay = 10
|
|
|
|
static let shared = PersistenceController.persistenceController
|
|
|
|
private static var persistenceController: PersistenceController {
|
|
#if targetEnvironment(simulator)
|
|
return PersistenceController(inMemory: false)
|
|
#else
|
|
return PersistenceController(inMemory: false)
|
|
#endif
|
|
|
|
}
|
|
|
|
public var viewContext: NSManagedObjectContext {
|
|
return PersistenceController.shared.container.viewContext
|
|
}
|
|
|
|
public func getEntry(byDate date: Date) -> 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.first
|
|
}
|
|
|
|
public func add(mood: Mood, forDate date: Date) {
|
|
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))
|
|
|
|
do {
|
|
try viewContext.save()
|
|
} catch {
|
|
let nsError = error as NSError
|
|
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func populateTestData() {
|
|
for idx in 1..<120 {
|
|
let newItem = MoodEntry(context: viewContext)
|
|
newItem.timestamp = 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))
|
|
}
|
|
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)")
|
|
}
|
|
}
|
|
|
|
func populateMemory() {
|
|
for idx in 1..<25 {
|
|
let newItem = MoodEntry(context: viewContext)
|
|
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))
|
|
}
|
|
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)")
|
|
}
|
|
}
|
|
|
|
func fillInMissingDates() {
|
|
let fetchRequest = NSFetchRequest<MoodEntry>(entityName: "MoodEntry")
|
|
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: false)]
|
|
let entries = try! viewContext.fetch(fetchRequest)
|
|
|
|
if let earliestDate = entries.last?.forDate {
|
|
let diffInDays = Calendar.current.dateComponents([.day], from: earliestDate, to: Date()).day
|
|
|
|
for idx in 1..<diffInDays! {
|
|
let searchDay = Calendar.current.date(byAdding: .day, value: -idx, to: Date())
|
|
if entries.filter({ Calendar.current.isDate($0.forDate!, inSameDayAs:searchDay!) }).isEmpty {
|
|
self.add(mood: .missing, forDate: searchDay!)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func clearDB() {
|
|
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "MoodEntry")
|
|
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
|
|
|
do {
|
|
try viewContext.executeAndMergeChanges(using: deleteRequest)
|
|
try viewContext.save()
|
|
} catch let error as NSError {
|
|
fatalError("Unresolved error \(error), \(error.userInfo)")
|
|
}
|
|
}
|
|
|
|
public func randomEntries(count: Int) -> [MoodEntry] {
|
|
var entries = [MoodEntry]()
|
|
|
|
for idx in 0..<count {
|
|
let newItem = MoodEntry(context: viewContext)
|
|
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))
|
|
entries.append(newItem)
|
|
}
|
|
return entries
|
|
}
|
|
|
|
let container: NSPersistentCloudKitContainer
|
|
|
|
init(inMemory: Bool = false) {
|
|
container = NSPersistentCloudKitContainer(name: "Feels")
|
|
if inMemory {
|
|
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
|
|
}
|
|
container.viewContext.automaticallyMergesChangesFromParent = true
|
|
|
|
for description in container.persistentStoreDescriptions {
|
|
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
|
|
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
|
|
}
|
|
|
|
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
|
|
if let error = error as NSError? {
|
|
// 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.
|
|
|
|
/*
|
|
Typical reasons for an error here include:
|
|
* The parent directory does not exist, cannot be created, or disallows writing.
|
|
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
|
|
* The device is out of space.
|
|
* The store could not be migrated to the current model version.
|
|
Check the error message to determine what the actual problem was.
|
|
*/
|
|
fatalError("Unresolved error \(error), \(error.userInfo)")
|
|
}
|
|
})
|
|
}
|
|
|
|
|
|
public func splitIntoYearMonth() -> [Int: [Int: [MoodEntry]]] {
|
|
let data = PersistenceController.shared.getData(startDate: Date(timeIntervalSince1970: 0),
|
|
endDate: Date(),
|
|
includedDays: [1,2,3,4,5,6,7]).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
|
|
let startDateOfMonth = Calendar.current.date(from: components)!
|
|
|
|
let items = data.filter({ entry in
|
|
let components = calendar.dateComponents([.month, .year], from: startDateOfMonth)
|
|
let entryComponents = calendar.dateComponents([.month, .year], from: entry.forDate!)
|
|
return (components.month == entryComponents.month && components.year == entryComponents.year)
|
|
})
|
|
if !items.isEmpty {
|
|
allMonths[month] = items
|
|
}
|
|
}
|
|
returnData[year] = allMonths
|
|
}
|
|
}
|
|
return returnData
|
|
}
|
|
}
|
|
|
|
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])
|
|
}
|
|
}
|