fix issue with two votes on the same date

fix issue with header not showing correct vote date
split logic for Persistence into different files
create class that deals with voting time, existing votes, and what should be shown based on that
This commit is contained in:
Trey t
2022-02-17 14:46:11 -06:00
parent f0ed56fe94
commit 675e44bca9
12 changed files with 547 additions and 427 deletions

View File

@@ -27,6 +27,16 @@
1C358FBE27B4D1F2002C83A6 /* CurrentStreakTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C358FBD27B4D1F2002C83A6 /* CurrentStreakTemplate.swift */; };
1C358FC027B4D20C002C83A6 /* MonthTotalTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C358FBF27B4D20C002C83A6 /* MonthTotalTemplate.swift */; };
1C358FC227B4D227002C83A6 /* WeekTotalTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C358FC127B4D227002C83A6 /* WeekTotalTemplate.swift */; };
1C4FF3BB27BEDDF000BE8F34 /* ShowBasedOnVoteLogics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4FF3BA27BEDDF000BE8F34 /* ShowBasedOnVoteLogics.swift */; };
1C4FF3BC27BEDF6600BE8F34 /* ShowBasedOnVoteLogics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4FF3BA27BEDDF000BE8F34 /* ShowBasedOnVoteLogics.swift */; };
1C4FF3BE27BEDF9100BE8F34 /* PersistenceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4FF3BD27BEDF9100BE8F34 /* PersistenceHelper.swift */; };
1C4FF3C027BEE06900BE8F34 /* PersistenceGET.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4FF3BF27BEE06900BE8F34 /* PersistenceGET.swift */; };
1C4FF3C127BEE06900BE8F34 /* PersistenceGET.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4FF3BF27BEE06900BE8F34 /* PersistenceGET.swift */; };
1C4FF3C327BEE07200BE8F34 /* PersistenceDELETE.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4FF3C227BEE07200BE8F34 /* PersistenceDELETE.swift */; };
1C4FF3C427BEE07200BE8F34 /* PersistenceDELETE.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4FF3C227BEE07200BE8F34 /* PersistenceDELETE.swift */; };
1C4FF3C727BEE09E00BE8F34 /* PersistenceADD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4FF3C627BEE09E00BE8F34 /* PersistenceADD.swift */; };
1C4FF3C827BEE09E00BE8F34 /* PersistenceADD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4FF3C627BEE09E00BE8F34 /* PersistenceADD.swift */; };
1C4FF3C927BEE0C300BE8F34 /* PersistenceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4FF3BD27BEDF9100BE8F34 /* PersistenceHelper.swift */; };
1C5F4976279C84090092F1B4 /* OnboardingData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C5F4975279C84090092F1B4 /* OnboardingData.swift */; };
1C5F4978279C945E0092F1B4 /* UserDefaultsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C5F4977279C945E0092F1B4 /* UserDefaultsStore.swift */; };
1C683FCA2792281400745862 /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C683FC92792281400745862 /* Stats.swift */; };
@@ -149,6 +159,11 @@
1C358FBD27B4D1F2002C83A6 /* CurrentStreakTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentStreakTemplate.swift; sourceTree = "<group>"; };
1C358FBF27B4D20C002C83A6 /* MonthTotalTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonthTotalTemplate.swift; sourceTree = "<group>"; };
1C358FC127B4D227002C83A6 /* WeekTotalTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekTotalTemplate.swift; sourceTree = "<group>"; };
1C4FF3BA27BEDDF000BE8F34 /* ShowBasedOnVoteLogics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowBasedOnVoteLogics.swift; sourceTree = "<group>"; };
1C4FF3BD27BEDF9100BE8F34 /* PersistenceHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceHelper.swift; sourceTree = "<group>"; };
1C4FF3BF27BEE06900BE8F34 /* PersistenceGET.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceGET.swift; sourceTree = "<group>"; };
1C4FF3C227BEE07200BE8F34 /* PersistenceDELETE.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceDELETE.swift; sourceTree = "<group>"; };
1C4FF3C627BEE09E00BE8F34 /* PersistenceADD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceADD.swift; sourceTree = "<group>"; };
1C5F4975279C84090092F1B4 /* OnboardingData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingData.swift; sourceTree = "<group>"; };
1C5F4977279C945E0092F1B4 /* UserDefaultsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsStore.swift; sourceTree = "<group>"; };
1C683FC92792281400745862 /* Stats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stats.swift; sourceTree = "<group>"; };
@@ -275,6 +290,18 @@
path = SharingTemplates;
sourceTree = "<group>";
};
1C4FF3C527BEE07800BE8F34 /* Persisence */ = {
isa = PBXGroup;
children = (
1CD90AEF278C7DDF001C4FEA /* Persistence.swift */,
1C4FF3C627BEE09E00BE8F34 /* PersistenceADD.swift */,
1C4FF3C227BEE07200BE8F34 /* PersistenceDELETE.swift */,
1C4FF3BF27BEE06900BE8F34 /* PersistenceGET.swift */,
1C4FF3BD27BEDF9100BE8F34 /* PersistenceHelper.swift */,
);
path = Persisence;
sourceTree = "<group>";
};
1CA03771279A291F00D26164 /* Onboarding */ = {
isa = PBXGroup;
children = (
@@ -345,9 +372,10 @@
1C744F2B278CE15600953A57 /* AppDelegate.swift */,
1CC469A9278F30A0003E0C6E /* BGTask.swift */,
1CD90B75278C8119001C4FEA /* LocalNotification.swift */,
1CD90AEF278C7DDF001C4FEA /* Persistence.swift */,
1C4FF3C527BEE07800BE8F34 /* Persisence */,
1CD90B5C278C7EAD001C4FEA /* Random.swift */,
1C683FC92792281400745862 /* Stats.swift */,
1C4FF3BA27BEDDF000BE8F34 /* ShowBasedOnVoteLogics.swift */,
1CA03771279A291F00D26164 /* Onboarding */,
1C26190127960CDA00FDC148 /* Protocols */,
1CAD602A27A5C1C800C520BD /* Views */,
@@ -639,6 +667,7 @@
1CAD603C27A5C1C800C520BD /* HeaderStatsView.swift in Sources */,
1CAD603827A5C1C800C520BD /* AddMoodHeaderView.swift in Sources */,
1CA0377C279B605000D26164 /* OnboardingWrapup.swift in Sources */,
1C4FF3C327BEE07200BE8F34 /* PersistenceDELETE.swift in Sources */,
1CA03775279A294800D26164 /* OnboardingDay.swift in Sources */,
1CAD603727A5C1C800C520BD /* FilterView.swift in Sources */,
1C683FCA2792281400745862 /* Stats.swift in Sources */,
@@ -646,6 +675,7 @@
1CD90B76278C8119001C4FEA /* LocalNotification.swift in Sources */,
1C358FB627B0AE15002C83A6 /* AllMoodsTotalTemplate.swift in Sources */,
1CD90B16278C7DE0001C4FEA /* Feels.xcdatamodeld in Sources */,
1C4FF3BE27BEDF9100BE8F34 /* PersistenceHelper.swift in Sources */,
1CC469AA278F30A0003E0C6E /* BGTask.swift in Sources */,
1CAD603B27A5C1C800C520BD /* ContentView.swift in Sources */,
1C5F4976279C84090092F1B4 /* OnboardingData.swift in Sources */,
@@ -658,6 +688,8 @@
1C744F2C278CE15600953A57 /* AppDelegate.swift in Sources */,
1CD90B63278C7EBA001C4FEA /* Mood.swift in Sources */,
1C358FBE27B4D1F2002C83A6 /* CurrentStreakTemplate.swift in Sources */,
1C4FF3C727BEE09E00BE8F34 /* PersistenceADD.swift in Sources */,
1C4FF3BB27BEDDF000BE8F34 /* ShowBasedOnVoteLogics.swift in Sources */,
1CAD603527A5C1C800C520BD /* SettingsView.swift in Sources */,
1CD90B53278C7E7A001C4FEA /* FeelsWidget.intentdefinition in Sources */,
1CC469AC27907D48003E0C6E /* DayChartView.swift in Sources */,
@@ -677,6 +709,7 @@
1CD90B18278C7DE0001C4FEA /* FeelsApp.swift in Sources */,
1C358FC027B4D20C002C83A6 /* MonthTotalTemplate.swift in Sources */,
1CA03777279A295600D26164 /* OnboardingTitle.swift in Sources */,
1C4FF3C027BEE06900BE8F34 /* PersistenceGET.swift in Sources */,
1CEC967127B9C2BB00CC8688 /* CustomIcon.swift in Sources */,
1C358FAD27ADD0C3002C83A6 /* Theme.swift in Sources */,
1C02589C27B9677A00EB91AC /* CreateIconView.swift in Sources */,
@@ -722,6 +755,8 @@
files = (
1CD90B65278C7EBA001C4FEA /* Mood.swift in Sources */,
1CEC967227B9C9FB00CC8688 /* IconView.swift in Sources */,
1C4FF3BC27BEDF6600BE8F34 /* ShowBasedOnVoteLogics.swift in Sources */,
1C4FF3C927BEE0C300BE8F34 /* PersistenceHelper.swift in Sources */,
1CA2662D2793908700C0E12C /* Persistence.swift in Sources */,
1CD90B5F278C7EAD001C4FEA /* Random.swift in Sources */,
1CD90B68278C7EBA001C4FEA /* MoodEntryExtension.swift in Sources */,
@@ -731,6 +766,9 @@
1C683FCB2792281400745862 /* Stats.swift in Sources */,
1CEC967327B9CA0C00CC8688 /* CustomIcon.swift in Sources */,
1C10E25027A1AB220047948B /* OnboardingDay.swift in Sources */,
1C4FF3C427BEE07200BE8F34 /* PersistenceDELETE.swift in Sources */,
1C4FF3C827BEE09E00BE8F34 /* PersistenceADD.swift in Sources */,
1C4FF3C127BEE06900BE8F34 /* PersistenceGET.swift in Sources */,
1C10E24F27A1AB1D0047948B /* OnboardingData.swift in Sources */,
1CD90B52278C7E7A001C4FEA /* FeelsWidget.intentdefinition in Sources */,
1CD90B4D278C7E7A001C4FEA /* FeelsWidget.swift in Sources */,

View File

@@ -36,6 +36,8 @@ struct FeelsApp: App {
if phase == .active {
UIApplication.shared.applicationIconBadgeNumber = 0
print("UserDefaultsStore input day", UserDefaultsStore.getOnboarding().inputDay)
print("UserDefaultsStore input date", UserDefaultsStore.getOnboarding().date)
}
}
}

View File

@@ -47,51 +47,12 @@ class ContentModeViewModel: ObservableObject {
grouped = PersistenceController.shared.splitIntoYearMonth()
numberOfItems = numberOfEntries
}
public func shouldShowVotingHeader() -> Bool {
if isMissingCurrentVote() {
return true
}
return savedOnboardingData.ableToVoteBasedOnCurentTime() ? true : false
}
public func updateOnboardingData(onboardingData: OnboardingData) {
self.savedOnboardingData = UserDefaultsStore.saveOnboarding(onboardingData: onboardingData)
LocalNotification.scheduleReminder(atTime: onboardingData.date, withTitle: onboardingData.title)
}
private func isMissingCurrentVote() -> Bool {
let latestVoteUnLocked = UserDefaultsStore.getOnboarding().ableToVoteBasedOnCurentTime()
let inputDay = UserDefaultsStore.getOnboarding().inputDay
var startDate: Date?
switch (latestVoteUnLocked, inputDay) {
case (true, .Previous):
startDate = Calendar.current.date(byAdding: .day, value: -1, to: Date())!
case (true, .Today):
startDate = Date()
case (false, .Previous):
startDate = Calendar.current.date(byAdding: .day, value: -2, to: Date())!
case (false, .Today):
startDate = Calendar.current.date(byAdding: .day, value: -1, to: Date())!
}
startDate = Calendar.current.startOfDay(for: startDate!)
let endDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate!)!
let fetchRequest = NSFetchRequest<MoodEntry>(entityName: "MoodEntry")
let fromPredicate = NSPredicate(format: "%@ <= %K", startDate!
as NSDate, #keyPath(MoodEntry.forDate))
let toPredicate = NSPredicate(format: "%K < %@", #keyPath(MoodEntry.forDate), endDate as NSDate)
let datePredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fromPredicate, toPredicate])
fetchRequest.predicate = datePredicate
let entries = try! PersistenceController.shared.viewContext.count(for: fetchRequest)
return entries == 0
}
public func updateData() {
getGroupedData()
}

View File

@@ -0,0 +1,104 @@
//
// 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 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()
}
}
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 {
var storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.groupShareId)
storeURL = storeURL?.appendingPathComponent("Feels.sqlite")
#if DEBUG
storeURL = storeURL?.appendingPathComponent("Feels-Debug.sqlite")
#endif
return storeURL!
}
}

View File

@@ -0,0 +1,63 @@
//
// PersistenceADD.swift
// Feels
//
// Created by Trey Tartt on 2/17/22.
//
import CoreData
extension PersistenceController {
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)
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
func fillInMissingDates() {
let endDate = ShowBasedOnVoteLogics.getLastDateVoteShouldExist()
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, to: endDate).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(mood: .missing, forDate: date, entryType: .listView)
}
}
}
}

View File

@@ -0,0 +1,29 @@
//
// 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)
try viewContext.save()
} catch let error as NSError {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
func delete(forDate: Date) {
if let entry = PersistenceController.shared.getEntry(byDate: forDate) {
viewContext.delete(entry)
try! viewContext.save()
}
}
}

View File

@@ -0,0 +1,92 @@
//
// 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() -> [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
}
}

View File

@@ -0,0 +1,85 @@
//
// PersistenceHelper.swift
// Feels (iOS)
//
// Created by Trey Tartt on 2/17/22.
//
import CoreData
extension PersistenceController {
private var childContext: NSManagedObjectContext {
return NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
}
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() {
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 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 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
}
}

View File

@@ -1,340 +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: false)
}
public var viewContext: NSManagedObjectContext {
return PersistenceController.shared.container.viewContext
}
private var childContext: NSManagedObjectContext {
return NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
}
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, 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)
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 latestVoteUnLocked = UserDefaultsStore.getOnboarding().ableToVoteBasedOnCurentTime()
let inputDay = UserDefaultsStore.getOnboarding().inputDay
var endDate: Date?
switch (latestVoteUnLocked, inputDay) {
case (true, .Previous):
endDate = Calendar.current.date(byAdding: .day, value: -1, to: Date())!
case (true, .Today):
endDate = Date()
case (false, .Previous):
endDate = Calendar.current.date(byAdding: .day, value: -2, to: Date())!
case (false, .Today):
endDate = Calendar.current.date(byAdding: .day, value: -1, to: Date())!
}
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, to: endDate!).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(mood: .missing, forDate: date, entryType: .listView)
}
}
}
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: 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
}
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)
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()
}
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])
}
}
extension PersistenceController {
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
}
}
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!
}
}

View File

@@ -0,0 +1,123 @@
//
// ShowBasedOnVoteLogics.swift
// Feels (iOS)
//
// Created by Trey Tartt on 2/17/22.
//
import CoreData
import SwiftUI
//if this is being shown we're missing an entry
// voting time is noon
// vote for current day
// today at 11 am -> How as yesterday
// today at 1 pm -> How is today
// vote for previous day
// today at 11 am -> How as 2 days ago
// today at 1 pm -> How was yesterday
class ShowBasedOnVoteLogics {
private static var currentVoting: (passTimeToVote: Bool, dayOptions: DayOptions) {
let passedTimeToVote = UserDefaultsStore.getOnboarding().ableToVoteBasedOnCurentTime()
let inputDay = UserDefaultsStore.getOnboarding().inputDay
return (passedTimeToVote, inputDay)
}
static func isMissingCurrentVote() -> Bool {
var startDate: Date?
switch (currentVoting.passTimeToVote, currentVoting.dayOptions) {
case (true, .Previous):
// if we're passed time to vote and the voting type is previous - last vote should be -1
startDate = Calendar.current.date(byAdding: .day, value: -1, to: Date())!
case (true, .Today):
// if we're passed time to vote and the voting type is today - last vote should be current date
startDate = Date()
case (false, .Previous):
// if we're NOT passed time to vote and the voting type is previous - last vote should be 2 days ago
startDate = Calendar.current.date(byAdding: .day, value: -2, to: Date())!
case (false, .Today):
// if we're NOT passed time to vote and the voting type is previous - last vote should be -1
startDate = Calendar.current.date(byAdding: .day, value: -1, to: Date())!
}
startDate = Calendar.current.startOfDay(for: startDate!)
let endDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate!)!
let fetchRequest = NSFetchRequest<MoodEntry>(entityName: "MoodEntry")
let fromPredicate = NSPredicate(format: "%@ <= %K", startDate!
as NSDate, #keyPath(MoodEntry.forDate))
let toPredicate = NSPredicate(format: "%K < %@", #keyPath(MoodEntry.forDate), endDate as NSDate)
let datePredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fromPredicate, toPredicate])
fetchRequest.predicate = datePredicate
let entries = try! PersistenceController.shared.viewContext.count(for: fetchRequest)
return entries < 1
}
static func getVotingTitle() -> String {
switch (currentVoting.passTimeToVote, currentVoting.dayOptions) {
case (true, .Previous):
// if we're passed time to vote and the voting type is previous - last vote should be -1
return "how was yesterday"
case (true, .Today):
// if we're passed time to vote and the voting type is previous - last vote should be today
return "how is today"
case (false, .Previous):
// if we're passed time to vote and the voting type is previous - last vote should be -2
let lastDayVoteShouldExist = ShowBasedOnVoteLogics.getLastDateVoteShouldExist()
return "how was \(Random.weekdayName(fromDate: lastDayVoteShouldExist))"
case (false, .Today):
// if we're passed time to vote and the voting type is previous - last vote should be -1
return "how as yesterday"
}
}
static func dateForHeaderVote() -> Date? {
var date: Date?
switch (currentVoting.passTimeToVote, currentVoting.dayOptions) {
case (true, .Previous):
// if we're passed time to vote and the voting type is previous - last vote should be -1
date = Calendar.current.date(byAdding: .day, value: -1, to: Date())
case (true, .Today):
// if we're passed time to vote and the voting type is previous - last vote should be today
date = Date()
case (false, .Previous):
// if we're passed time to vote and the voting type is previous - last vote should be -2
date = Calendar.current.date(byAdding: .day, value: -2, to: Date())
case (false, .Today):
// if we're passed time to vote and the voting type is previous - last vote should be -1
date = Calendar.current.date(byAdding: .day, value: -1, to: Date())
}
date = Calendar.current.date(byAdding: .day, value: -2, to: Date())
if let date = date {
return date
}
return nil
}
static func getLastDateVoteShouldExist() -> Date {
var endDate: Date?
switch (currentVoting.passTimeToVote, currentVoting.dayOptions) {
case (true, .Previous):
// if we're passed time to vote and the voting type is previous - last vote should -1
endDate = Calendar.current.date(byAdding: .day, value: -1, to: Date())!
case (true, .Today):
// if we're passed time to vote and the voting type is previous - last vote should be today
endDate = Date()
case (false, .Previous):
// if we're passed time to vote and the voting type is previous - last vote should be -2
endDate = Calendar.current.date(byAdding: .day, value: -2, to: Date())!
case (false, .Today):
// if we're passed time to vote and the voting type is previous - last vote should be -1
endDate = Calendar.current.date(byAdding: .day, value: -1, to: Date())!
}
return endDate!
}
}

View File

@@ -25,7 +25,7 @@ struct AddMoodHeaderView: View {
Color(theme.currentTheme.secondaryBGColor)
VStack {
Text(self.getTitle())
Text(ShowBasedOnVoteLogics.getVotingTitle())
.font(.title)
.foregroundColor(Color(UIColor.label))
.padding()
@@ -56,48 +56,8 @@ struct AddMoodHeaderView: View {
.frame(minWidth: 0, maxWidth: .infinity)
}
private func getTitle() -> String {
//if this is being shown we're missing an entry
// voting time is noon
// vote for current day
// today at 11 am -> How as yesterday
// today at 1 pm -> How is today
// vote for previous day
// today at 11 am -> How as 2 days ago
// today at 1 pm -> How was yesterday
let latestVoteUnLocked = UserDefaultsStore.getOnboarding().ableToVoteBasedOnCurentTime()
let inputDay = UserDefaultsStore.getOnboarding().inputDay
switch (latestVoteUnLocked, inputDay) {
case (true, .Previous):
return "how was yesterday"
case (true, .Today):
return "how is today"
case (false, .Previous):
return "how was two days ago"
case (false, .Today):
return "how as yesterday"
}
}
private func addItem(withMood mood: Mood) {
let latestVoteUnLocked = UserDefaultsStore.getOnboarding().ableToVoteBasedOnCurentTime()
let inputDay = UserDefaultsStore.getOnboarding().inputDay
var date: Date?
switch (latestVoteUnLocked, inputDay) {
case (true, .Previous):
date = Calendar.current.date(byAdding: .day, value: -1, to: Date())!
case (true, .Today):
date = Date()
case (false, .Previous):
date = Calendar.current.date(byAdding: .day, value: -2, to: Date())!
case (false, .Today):
date = Calendar.current.date(byAdding: .day, value: -1, to: Date())!
}
if let date = date {
if let date = ShowBasedOnVoteLogics.dateForHeaderVote() {
addItemHeaderClosure(mood, date)
}
}

View File

@@ -91,8 +91,7 @@ struct ContentView: View {
}
if let selectedEntry = selectedEntry,
deleteEnabled,
selectedEntry.mood != .missing {
deleteEnabled{
Button(String(localized: "content_view_delete_entry"), action: {
viewModel.update(entry: selectedEntry, toMood: Mood.missing)
showUpdateEntryAlert = false
@@ -140,7 +139,11 @@ struct ContentView: View {
return ""
}
let components = Calendar.current.dateComponents([.day, .month, .year], from: entry.forDate!)
guard let forDate = entry.forDate else {
return ""
}
let components = Calendar.current.dateComponents([.day, .month, .year], from: forDate)
// let day = components.day!
let month = components.month!
let year = components.year!
@@ -178,7 +181,7 @@ struct ContentView: View {
private var headerView: some View {
VStack {
if viewModel.shouldShowVotingHeader() {
if ShowBasedOnVoteLogics.isMissingCurrentVote() {
AddMoodHeaderView(addItemHeaderClosure: { (mood, date) in
withAnimation {
viewModel.add(mood: mood, forDate: date, entryType: .header)