Files
Reflect/Shared/Persistence.swift
Trey t 0109aee8f8 switch db between debug and release
on widgets if its before the voting time show yesterdays vote, if after either show no vote or current vote

user shared user defaults
2022-01-28 10:27:33 -06:00

289 lines
12 KiB
Swift

//
// 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: false)
}
public var viewContext: NSManagedObjectContext {
return PersistenceController.shared.container.viewContext
}
public var switchContainerListeners = [(() -> Void)]()
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))
newItem.canEdit = true
newItem.canDelete = true
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)
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)")
}
}
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)
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)")
}
}
func fillInMissingDates() {
let fetchRequest = NSFetchRequest<MoodEntry>(entityName: "MoodEntry")
let entries = try! viewContext.fetch(fetchRequest)
if let earliestDate = entries.last?.forDate,
let diffInDays = Calendar.current.dateComponents([.day], from: earliestDate, to: Date()).day,
diffInDays > 1 {
for idx in 1..<diffInDays {
if let searchDay = Calendar.current.date(byAdding: .day, value: -idx, to: Date()),
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))
newItem.canEdit = true
newItem.canDelete = true
entries.append(newItem)
}
return entries
}
lazy var container: NSPersistentContainer = {
setupContainer()
}()
func switchContainer() {
try? viewContext.save()
container = setupContainer()
for item in switchContainerListeners {
item()
}
}
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)
}
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()
}
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])
}
}
class NSCustomPersistentContainer: NSPersistentContainer {
override open class func defaultDirectoryURL() -> URL {
var storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.groupShareId)
storeURL = storeURL?.appendingPathComponent("Feels.sqlite")
#if DEBUG
storeURL = storeURL?.appendingPathComponent("Feels-Debug.sqlite")
#endif
return storeURL!
}
}