From 64dd1855ac4251752213ed92b88ac67b4422039e Mon Sep 17 00:00:00 2001 From: Trey t Date: Sat, 15 Jan 2022 12:03:31 -0600 Subject: [PATCH] WIP - a lot of uncommitted work --- Feels.xcodeproj/project.pbxproj | 44 ++- .../xcshareddata/swiftpm/Package.resolved | 22 +- FeelsWidget/FeelsWidget.swift | 24 +- Shared/AppDelegate.swift | 57 +++ .../average.imageset/Contents.json | 24 ++ .../average.imageset/meh-regular.svg | 1 + .../bad.imageset/Contents.json | 24 ++ .../bad.imageset/frown-regular.svg | 1 + .../good.imageset/Contents.json | 24 ++ .../good.imageset/grin-regular.svg | 1 + .../great.imageset/Contents.json | 24 ++ .../great.imageset/smile-beam-regular.svg | 1 + .../horrible.imageset/Contents.json | 24 ++ .../horrible.imageset/sad-tear-regular.svg | 1 + .../missing.imageset/Contents.json | 21 + .../Screen Shot 2022-01-13 at 5.02.34 PM.png | Bin 0 -> 6819 bytes Shared/BGTask.swift | 40 ++ .../Shared.xcdatamodel/contents | 5 +- Shared/FeelsApp.swift | 36 +- Shared/LocalNotification.swift | 35 -- Shared/Models/Mood.swift | 31 +- Shared/Persistence.swift | 60 ++- Shared/Random.swift | 10 + Shared/Stats.swift | 17 + Shared/views/AddMoodHeaderView.swift | 8 +- Shared/views/CircleView.swift | 41 ++ Shared/views/ContentView.swift | 20 +- Shared/views/FilterView.swift | 362 ++++++++++++++++++ Shared/views/GraphView.swift | 130 ++++++- Shared/views/HeaderStatsView.swift | 2 +- Shared/views/SettingsView.swift | 88 +++-- 31 files changed, 1024 insertions(+), 154 deletions(-) create mode 100644 Shared/AppDelegate.swift create mode 100644 Shared/Assets.xcassets/average.imageset/Contents.json create mode 100644 Shared/Assets.xcassets/average.imageset/meh-regular.svg create mode 100644 Shared/Assets.xcassets/bad.imageset/Contents.json create mode 100644 Shared/Assets.xcassets/bad.imageset/frown-regular.svg create mode 100644 Shared/Assets.xcassets/good.imageset/Contents.json create mode 100644 Shared/Assets.xcassets/good.imageset/grin-regular.svg create mode 100644 Shared/Assets.xcassets/great.imageset/Contents.json create mode 100644 Shared/Assets.xcassets/great.imageset/smile-beam-regular.svg create mode 100644 Shared/Assets.xcassets/horrible.imageset/Contents.json create mode 100644 Shared/Assets.xcassets/horrible.imageset/sad-tear-regular.svg create mode 100644 Shared/Assets.xcassets/missing.imageset/Contents.json create mode 100644 Shared/Assets.xcassets/missing.imageset/Screen Shot 2022-01-13 at 5.02.34 PM.png create mode 100644 Shared/BGTask.swift create mode 100644 Shared/Stats.swift create mode 100644 Shared/views/CircleView.swift create mode 100644 Shared/views/FilterView.swift diff --git a/Feels.xcodeproj/project.pbxproj b/Feels.xcodeproj/project.pbxproj index fbd9f26..33d2ea0 100644 --- a/Feels.xcodeproj/project.pbxproj +++ b/Feels.xcodeproj/project.pbxproj @@ -7,6 +7,15 @@ objects = { /* Begin PBXBuildFile section */ + 1C412080278E23CC00D9153A /* Charts in Frameworks */ = {isa = PBXBuildFile; productRef = 1C41207F278E23CC00D9153A /* Charts */; }; + 1C412082278F2B8800D9153A /* FilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C412081278F2B8800D9153A /* FilterView.swift */; }; + 1C412083278F2B8800D9153A /* FilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C412081278F2B8800D9153A /* FilterView.swift */; }; + 1C683FCA2792281400745862 /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C683FC92792281400745862 /* Stats.swift */; }; + 1C683FCB2792281400745862 /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C683FC92792281400745862 /* Stats.swift */; }; + 1C683FCC2792281400745862 /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C683FC92792281400745862 /* Stats.swift */; }; + 1C744F2C278CE15600953A57 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C744F2B278CE15600953A57 /* AppDelegate.swift */; }; + 1CC469AA278F30A0003E0C6E /* BGTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC469A9278F30A0003E0C6E /* BGTask.swift */; }; + 1CC469AC27907D48003E0C6E /* CircleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC469AB27907D48003E0C6E /* CircleView.swift */; }; 1CD90B07278C7DE0001C4FEA /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CD90B06278C7DE0001C4FEA /* Tests_iOS.swift */; }; 1CD90B09278C7DE0001C4FEA /* Tests_iOSLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CD90B08278C7DE0001C4FEA /* Tests_iOSLaunchTests.swift */; }; 1CD90B13278C7DE0001C4FEA /* Tests_macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CD90B12278C7DE0001C4FEA /* Tests_macOS.swift */; }; @@ -51,7 +60,6 @@ 1CD90B71278C80CA001C4FEA /* Feels.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1CD90AEB278C7DDF001C4FEA /* Feels.xcdatamodeld */; }; 1CD90B76278C8119001C4FEA /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CD90B75278C8119001C4FEA /* LocalNotification.swift */; }; 1CD90B77278C8119001C4FEA /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CD90B75278C8119001C4FEA /* LocalNotification.swift */; }; - 1CD90B7B278C8146001C4FEA /* Charts in Frameworks */ = {isa = PBXBuildFile; productRef = 1CD90B7A278C8146001C4FEA /* Charts */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -93,6 +101,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 1C412081278F2B8800D9153A /* FilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterView.swift; sourceTree = ""; }; + 1C683FC92792281400745862 /* Stats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stats.swift; sourceTree = ""; }; + 1C744F2B278CE15600953A57 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 1CC469A9278F30A0003E0C6E /* BGTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGTask.swift; sourceTree = ""; }; + 1CC469AB27907D48003E0C6E /* CircleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleView.swift; sourceTree = ""; }; 1CD90AEC278C7DDF001C4FEA /* Shared.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Shared.xcdatamodel; sourceTree = ""; }; 1CD90AED278C7DDF001C4FEA /* FeelsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeelsApp.swift; sourceTree = ""; }; 1CD90AEF278C7DDF001C4FEA /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; @@ -136,7 +149,7 @@ buildActionMask = 2147483647; files = ( 1CD90B6C278C7F78001C4FEA /* CloudKit.framework in Frameworks */, - 1CD90B7B278C8146001C4FEA /* Charts in Frameworks */, + 1C412080278E23CC00D9153A /* Charts in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -196,9 +209,12 @@ isa = PBXGroup; children = ( 1CD90AED278C7DDF001C4FEA /* FeelsApp.swift */, + 1C744F2B278CE15600953A57 /* AppDelegate.swift */, + 1CC469A9278F30A0003E0C6E /* BGTask.swift */, 1CD90B75278C8119001C4FEA /* LocalNotification.swift */, 1CD90AEF278C7DDF001C4FEA /* Persistence.swift */, 1CD90B5C278C7EAD001C4FEA /* Random.swift */, + 1C683FC92792281400745862 /* Stats.swift */, 1CD90B31278C7E38001C4FEA /* views */, 1CD90B60278C7EBA001C4FEA /* Models */, 1CD90AF0278C7DE0001C4FEA /* Assets.xcassets */, @@ -250,9 +266,11 @@ children = ( 1CD90B34278C7E38001C4FEA /* AddMoodHeaderView.swift */, 1CD90B35278C7E38001C4FEA /* ContentView.swift */, + 1C412081278F2B8800D9153A /* FilterView.swift */, 1CD90B33278C7E38001C4FEA /* GraphView.swift */, 1CD90B36278C7E38001C4FEA /* HeaderStatsView.swift */, 1CD90B32278C7E38001C4FEA /* SettingsView.swift */, + 1CC469AB27907D48003E0C6E /* CircleView.swift */, ); path = views; sourceTree = ""; @@ -306,7 +324,7 @@ ); name = "Feels (iOS)"; packageProductDependencies = ( - 1CD90B7A278C8146001C4FEA /* Charts */, + 1C41207F278E23CC00D9153A /* Charts */, ); productName = "Feels (iOS)"; productReference = 1CD90AF5278C7DE0001C4FEA /* Feels.app */; @@ -421,7 +439,7 @@ ); mainGroup = 1CD90AE5278C7DDF001C4FEA; packageReferences = ( - 1CD90B79278C8146001C4FEA /* XCRemoteSwiftPackageReference "Charts" */, + 1C41207E278E23CB00D9153A /* XCRemoteSwiftPackageReference "Charts" */, ); productRefGroup = 1CD90AF6278C7DE0001C4FEA /* Products */; projectDirPath = ""; @@ -483,17 +501,22 @@ buildActionMask = 2147483647; files = ( 1CD90B39278C7E38001C4FEA /* GraphView.swift in Sources */, + 1C683FCA2792281400745862 /* Stats.swift in Sources */, 1CD90B76278C8119001C4FEA /* LocalNotification.swift in Sources */, 1CD90B16278C7DE0001C4FEA /* Feels.xcdatamodeld in Sources */, + 1CC469AA278F30A0003E0C6E /* BGTask.swift in Sources */, 1CD90B5D278C7EAD001C4FEA /* Random.swift in Sources */, + 1C744F2C278CE15600953A57 /* AppDelegate.swift in Sources */, 1CD90B63278C7EBA001C4FEA /* Mood.swift in Sources */, 1CD90B53278C7E7A001C4FEA /* FeelsWidget.intentdefinition in Sources */, 1CD90B3D278C7E38001C4FEA /* ContentView.swift in Sources */, 1CD90B3F278C7E38001C4FEA /* HeaderStatsView.swift in Sources */, 1CD90B3B278C7E38001C4FEA /* AddMoodHeaderView.swift in Sources */, + 1CC469AC27907D48003E0C6E /* CircleView.swift in Sources */, 1CD90B37278C7E38001C4FEA /* SettingsView.swift in Sources */, 1CD90B66278C7EBA001C4FEA /* MoodEntryExtension.swift in Sources */, 1CD90B1C278C7DE0001C4FEA /* Persistence.swift in Sources */, + 1C412082278F2B8800D9153A /* FilterView.swift in Sources */, 1CD90B18278C7DE0001C4FEA /* FeelsApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -502,6 +525,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1C412083278F2B8800D9153A /* FilterView.swift in Sources */, 1CD90B64278C7EBA001C4FEA /* Mood.swift in Sources */, 1CD90B3A278C7E38001C4FEA /* GraphView.swift in Sources */, 1CD90B17278C7DE0001C4FEA /* Feels.xcdatamodeld in Sources */, @@ -522,6 +546,7 @@ buildActionMask = 2147483647; files = ( 1CD90B09278C7DE0001C4FEA /* Tests_iOSLaunchTests.swift in Sources */, + 1C683FCC2792281400745862 /* Stats.swift in Sources */, 1CD90B07278C7DE0001C4FEA /* Tests_iOS.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -544,6 +569,7 @@ 1CD90B5B278C7E91001C4FEA /* Persistence.swift in Sources */, 1CD90B68278C7EBA001C4FEA /* MoodEntryExtension.swift in Sources */, 1CD90B71278C80CA001C4FEA /* Feels.xcdatamodeld in Sources */, + 1C683FCB2792281400745862 /* Stats.swift in Sources */, 1CD90B52278C7E7A001C4FEA /* FeelsWidget.intentdefinition in Sources */, 1CD90B4D278C7E7A001C4FEA /* FeelsWidget.swift in Sources */, ); @@ -1014,20 +1040,20 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 1CD90B79278C8146001C4FEA /* XCRemoteSwiftPackageReference "Charts" */ = { + 1C41207E278E23CB00D9153A /* XCRemoteSwiftPackageReference "Charts" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/danielgindi/Charts"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 3.0.0; + branch = master; + kind = branch; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 1CD90B7A278C8146001C4FEA /* Charts */ = { + 1C41207F278E23CC00D9153A /* Charts */ = { isa = XCSwiftPackageProductDependency; - package = 1CD90B79278C8146001C4FEA /* XCRemoteSwiftPackageReference "Charts" */; + package = 1C41207E278E23CB00D9153A /* XCRemoteSwiftPackageReference "Charts" */; productName = Charts; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Feels.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Feels.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0878b14..d7cede6 100644 --- a/Feels.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Feels.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -4,10 +4,28 @@ { "package": "Charts", "repositoryURL": "https://github.com/danielgindi/Charts", + "state": { + "branch": "master", + "revision": "85cfba96eb3492124d645ac0901b66f0cb266267", + "version": null + } + }, + { + "package": "swift-algorithms", + "repositoryURL": "https://github.com/apple/swift-algorithms", "state": { "branch": null, - "revision": "66546404a6739173b8e436ab6bc1f2897cd08594", - "version": "3.6.0" + "revision": "2327673b0e9c7e90e6b1826376526ec3627210e4", + "version": "0.2.1" + } + }, + { + "package": "swift-numerics", + "repositoryURL": "https://github.com/apple/swift-numerics", + "state": { + "branch": null, + "revision": "6583ac70c326c3ee080c1d42d9ca3361dca816cd", + "version": "0.1.0" } } ] diff --git a/FeelsWidget/FeelsWidget.swift b/FeelsWidget/FeelsWidget.swift index c46edfd..4b4ea31 100644 --- a/FeelsWidget/FeelsWidget.swift +++ b/FeelsWidget/FeelsWidget.swift @@ -122,11 +122,11 @@ struct MediumWidgetView: View { Spacer() HStack { - Text(firstGroup.first?.date ?? Date(), formatter: formatter) + Text(firstGroup.first?.forDate ?? Date(), formatter: formatter) .font(.system(.footnote)) Text(" - ") .font(.system(.footnote)) - Text(firstGroup.last?.date ?? Date(), formatter: formatter) + Text(firstGroup.last?.forDate ?? Date(), formatter: formatter) .font(.system(.footnote)) } .frame(minWidth: 0, maxWidth: .infinity) @@ -164,11 +164,11 @@ struct LargeWidgetView: View { Spacer() HStack { - Text(firstGroup.first?.date ?? Date(), formatter: formatter) + Text(firstGroup.first?.forDate ?? Date(), formatter: formatter) .font(.system(.footnote)) Text(" - ") .font(.system(.footnote)) - Text(firstGroup.last?.date ?? Date(), formatter: formatter) + Text(firstGroup.last?.forDate ?? Date(), formatter: formatter) .font(.system(.footnote)) } .frame(minWidth: 0, maxWidth: .infinity) @@ -181,10 +181,10 @@ struct LargeWidgetView: View { Spacer() HStack { - Text(lastGroup.first?.date ?? Date(), formatter: formatter) + Text(lastGroup.first?.forDate ?? Date(), formatter: formatter) .font(.system(.footnote)) Text(" - ") - Text(lastGroup.last?.date ?? Date(), formatter: formatter) + Text(lastGroup.last?.forDate ?? Date(), formatter: formatter) .font(.system(.footnote)) } .frame(minWidth: 0, maxWidth: .infinity) @@ -244,20 +244,20 @@ struct FeelsWidget_Previews: PreviewProvider { static var previews: some View { Group { FeelsWidgetEntryView(entry: SimpleEntry(date: Date(), - configuration: ConfigurationIntent(), - mood: PersistenceController.shared.randomEntries(count: 1))) + configuration: ConfigurationIntent(), + mood: PersistenceController.shared.randomEntries(count: 1))) .previewContext(WidgetPreviewContext(family: .systemSmall)) .environment(\.sizeCategory, .small) FeelsWidgetEntryView(entry: SimpleEntry(date: Date(), - configuration: ConfigurationIntent(), - mood: PersistenceController.shared.randomEntries(count: 3))) + configuration: ConfigurationIntent(), + mood: PersistenceController.shared.randomEntries(count: 3))) .previewContext(WidgetPreviewContext(family: .systemMedium)) .environment(\.sizeCategory, .medium) FeelsWidgetEntryView(entry: SimpleEntry(date: Date(), - configuration: ConfigurationIntent(), - mood: PersistenceController.shared.randomEntries(count: 10))) + configuration: ConfigurationIntent(), + mood: PersistenceController.shared.randomEntries(count: 10))) .previewContext(WidgetPreviewContext(family: .systemLarge)) .environment(\.sizeCategory, .large) } diff --git a/Shared/AppDelegate.swift b/Shared/AppDelegate.swift new file mode 100644 index 0000000..97eff28 --- /dev/null +++ b/Shared/AppDelegate.swift @@ -0,0 +1,57 @@ +// +// AppDelegate.swift +// Feels (iOS) +// +// Created by Trey Tartt on 1/10/22. +// + +import Foundation +import UserNotifications +import UIKit +import WidgetKit + +// AppDelegate.swift +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { +// PersistenceController.shared.clearDB() + NotificationCenter.default.addObserver(self, + selector: #selector(fetchChanges), + name: .NSPersistentStoreRemoteChange, + object: PersistenceController.shared.container.persistentStoreCoordinator) + + application.registerForRemoteNotifications() + UNUserNotificationCenter.current().delegate = self + return true + } + + @objc func fetchChanges(note: Notification) { + WidgetCenter.shared.reloadAllTimelines() + } +} + +extension AppDelegate: UNUserNotificationCenterDelegate { + func requestAuthorization() { } + + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + completionHandler([.badge, .banner, .sound]) + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + if let action = LocalNotification.ActionType(rawValue: response.actionIdentifier) { + switch action { + case .horrible: + PersistenceController.shared.add(mood: .horrible, forDate: Date()) + case .bad: + PersistenceController.shared.add(mood: .bad, forDate: Date()) + case .average: + PersistenceController.shared.add(mood: .average, forDate: Date()) + case .good: + PersistenceController.shared.add(mood: .good, forDate: Date()) + case .great: + PersistenceController.shared.add(mood: .great, forDate: Date()) + } + } + WidgetCenter.shared.reloadAllTimelines() + completionHandler() + } +} diff --git a/Shared/Assets.xcassets/average.imageset/Contents.json b/Shared/Assets.xcassets/average.imageset/Contents.json new file mode 100644 index 0000000..00fb6a2 --- /dev/null +++ b/Shared/Assets.xcassets/average.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "filename" : "meh-regular.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Shared/Assets.xcassets/average.imageset/meh-regular.svg b/Shared/Assets.xcassets/average.imageset/meh-regular.svg new file mode 100644 index 0000000..9ad9cf9 --- /dev/null +++ b/Shared/Assets.xcassets/average.imageset/meh-regular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Shared/Assets.xcassets/bad.imageset/Contents.json b/Shared/Assets.xcassets/bad.imageset/Contents.json new file mode 100644 index 0000000..a8ac09d --- /dev/null +++ b/Shared/Assets.xcassets/bad.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "filename" : "frown-regular.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Shared/Assets.xcassets/bad.imageset/frown-regular.svg b/Shared/Assets.xcassets/bad.imageset/frown-regular.svg new file mode 100644 index 0000000..e32249b --- /dev/null +++ b/Shared/Assets.xcassets/bad.imageset/frown-regular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Shared/Assets.xcassets/good.imageset/Contents.json b/Shared/Assets.xcassets/good.imageset/Contents.json new file mode 100644 index 0000000..a685e3c --- /dev/null +++ b/Shared/Assets.xcassets/good.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "filename" : "grin-regular.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Shared/Assets.xcassets/good.imageset/grin-regular.svg b/Shared/Assets.xcassets/good.imageset/grin-regular.svg new file mode 100644 index 0000000..380aed4 --- /dev/null +++ b/Shared/Assets.xcassets/good.imageset/grin-regular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Shared/Assets.xcassets/great.imageset/Contents.json b/Shared/Assets.xcassets/great.imageset/Contents.json new file mode 100644 index 0000000..f3c745f --- /dev/null +++ b/Shared/Assets.xcassets/great.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "filename" : "smile-beam-regular.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Shared/Assets.xcassets/great.imageset/smile-beam-regular.svg b/Shared/Assets.xcassets/great.imageset/smile-beam-regular.svg new file mode 100644 index 0000000..3c09689 --- /dev/null +++ b/Shared/Assets.xcassets/great.imageset/smile-beam-regular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Shared/Assets.xcassets/horrible.imageset/Contents.json b/Shared/Assets.xcassets/horrible.imageset/Contents.json new file mode 100644 index 0000000..d7421a5 --- /dev/null +++ b/Shared/Assets.xcassets/horrible.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "filename" : "sad-tear-regular.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Shared/Assets.xcassets/horrible.imageset/sad-tear-regular.svg b/Shared/Assets.xcassets/horrible.imageset/sad-tear-regular.svg new file mode 100644 index 0000000..edae2d2 --- /dev/null +++ b/Shared/Assets.xcassets/horrible.imageset/sad-tear-regular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Shared/Assets.xcassets/missing.imageset/Contents.json b/Shared/Assets.xcassets/missing.imageset/Contents.json new file mode 100644 index 0000000..dabdec4 --- /dev/null +++ b/Shared/Assets.xcassets/missing.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Screen Shot 2022-01-13 at 5.02.34 PM.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shared/Assets.xcassets/missing.imageset/Screen Shot 2022-01-13 at 5.02.34 PM.png b/Shared/Assets.xcassets/missing.imageset/Screen Shot 2022-01-13 at 5.02.34 PM.png new file mode 100644 index 0000000000000000000000000000000000000000..f2e032602e875df1481ec9f7a3e6b25e3bcd6873 GIT binary patch literal 6819 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 dOfKUQsiq*6b)EJt - + + - + \ No newline at end of file diff --git a/Shared/FeelsApp.swift b/Shared/FeelsApp.swift index 3558f7e..27dcf5f 100644 --- a/Shared/FeelsApp.swift +++ b/Shared/FeelsApp.swift @@ -7,15 +7,17 @@ import SwiftUI import BackgroundTasks +import WidgetKit @main struct FeelsApp: App { @Environment(\.scenePhase) private var scenePhase + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + let persistenceController = PersistenceController.shared init() { // persistenceController.fillInMissingDates() - BGTaskScheduler.shared.cancelAllTaskRequests() BGTaskScheduler.shared.register(forTaskWithIdentifier: BGTask.updateDBMissingID, using: nil) { (task) in BGTask.runFillInMissingDatesTask(task: task as! BGProcessingTask) @@ -30,38 +32,8 @@ struct FeelsApp: App { if phase == .background { BGTask.scheduleBackgroundProcessing() print("background") + WidgetCenter.shared.reloadAllTimelines() } } } } - -class BGTask { - static let updateDBMissingID = "com.88oak.Feels.dbUpdateMissing" - - class func runFillInMissingDatesTask(task: BGProcessingTask) { - BGTask.scheduleBackgroundProcessing() - - task.expirationHandler = { - task.setTaskCompleted(success: false) - } - - PersistenceController.shared.fillInMissingDates() - task.setTaskCompleted(success: true) - } - - class func scheduleBackgroundProcessing() { - let request = BGProcessingTaskRequest(identifier: BGTask.updateDBMissingID) - request.requiresNetworkConnectivity = false - request.requiresExternalPower = false - - var runDate = Calendar.current.date(byAdding: .day, value: 1, to: Date()) - runDate = Calendar.current.date(bySettingHour: 0, minute: 1, second: 0, of: runDate!) - request.earliestBeginDate = runDate - - do { - try BGTaskScheduler.shared.submit(request) - } catch { - print("Could not schedule image fetch: (error)") - } - } -} diff --git a/Shared/LocalNotification.swift b/Shared/LocalNotification.swift index c5a41b2..d962843 100644 --- a/Shared/LocalNotification.swift +++ b/Shared/LocalNotification.swift @@ -106,38 +106,3 @@ class LocalNotification { UNUserNotificationCenter.current().removeAllPendingNotificationRequests() } } - -class NotificationDelegate: NSObject, ObservableObject, UNUserNotificationCenterDelegate { - @Published var notificationCounter = 0 - - override init() { - super.init() - UNUserNotificationCenter.current().delegate = self - } - - func requestAuthorization() { - - } - - func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - completionHandler([.badge, .banner, .sound]) - } - - func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - if let action = LocalNotification.ActionType(rawValue: response.actionIdentifier) { - switch action { - case .horrible: - PersistenceController.shared.add(mood: .horrible, forDate: Date()) - case .bad: - PersistenceController.shared.add(mood: .bad, forDate: Date()) - case .average: - PersistenceController.shared.add(mood: .average, forDate: Date()) - case .good: - PersistenceController.shared.add(mood: .good, forDate: Date()) - case .great: - PersistenceController.shared.add(mood: .great, forDate: Date()) - } - } - completionHandler() - } -} diff --git a/Shared/Models/Mood.swift b/Shared/Models/Mood.swift index 1c407d8..be011e0 100644 --- a/Shared/Models/Mood.swift +++ b/Shared/Models/Mood.swift @@ -33,25 +33,42 @@ enum Mood: Int { } } + var color: Color { + switch self { + case .horrible: + return .red + case .bad: + return .orange + case .average: + return .blue + case .good: + return .yellow + case .great: + return .green + case .missing: + return Color(uiColor: UIColor.tertiarySystemBackground) + } + } + static var allValues: [Mood] { return [Mood.horrible, Mood.bad, Mood.average, Mood.good, Mood.great] } - var icon: Text { + var icon: Image { switch self { case .horrible: - return Text("😫") + return Image("horrible", bundle: .main) case .bad: - return Text("🙁") + return Image("bad", bundle: .main) case .average: - return Text("😐") + return Image("average", bundle: .main) case .good: - return Text("🙂") + return Image("good", bundle: .main) case .great: - return Text("😆") + return Image("great", bundle: .main) case .missing: - return Text("🚫") + return Image("missing", bundle: .main) } } } diff --git a/Shared/Persistence.swift b/Shared/Persistence.swift index d4024a1..e834d1f 100644 --- a/Shared/Persistence.swift +++ b/Shared/Persistence.swift @@ -27,7 +27,8 @@ struct PersistenceController { let newItem = MoodEntry(context: viewContext) newItem.timestamp = Date() newItem.moodValue = Int16(mood.rawValue) - newItem.date = date + newItem.forDate = date + newItem.weekDay = Int16(Calendar.current.component(.weekday, from: date)) do { try viewContext.save() @@ -39,9 +40,9 @@ struct PersistenceController { public func moodEntries(forStartDate date: Date, count: Int) -> [MoodEntry] { let fetchRequest = NSFetchRequest(entityName: "MoodEntry") - + fetchRequest.fetchLimit = count - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)] + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: false)] // var calendar = Calendar.current // calendar.timeZone = NSTimeZone.local @@ -59,12 +60,42 @@ struct PersistenceController { } } + public var earliestEntry: MoodEntry? { + let fetchRequest = NSFetchRequest(entityName: "MoodEntry") + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)] + let first = try! viewContext.fetch(fetchRequest).first + return first ?? nil + } + + public var latestEntry: MoodEntry? { + let fetchRequest = NSFetchRequest(entityName: "MoodEntry") + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)] + let last = try! viewContext.fetch(fetchRequest).last + return last ?? nil + } + + public func getData(startDate: Date, endDate: Date) -> [MoodEntry] { + let predicate = NSPredicate(format: "%K >= %@ && %K <= %@", + "forDate", + startDate as NSDate, + "forDate", + endDate as NSDate) + + let fetchRequest = NSFetchRequest(entityName: "MoodEntry") + fetchRequest.predicate = predicate + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)] + return try! viewContext.fetch(fetchRequest) + } + func populateTestData() { for idx in 1..<25 { let newItem = MoodEntry(context: viewContext) newItem.timestamp = Date() newItem.moodValue = Int16(Mood.allValues.randomElement()!.rawValue) - newItem.date = Calendar.current.date(byAdding: .day, value: -idx, to: Date()) + + 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() @@ -81,7 +112,10 @@ struct PersistenceController { let newItem = MoodEntry(context: viewContext) newItem.timestamp = Calendar.current.date(byAdding: .day, value: -idx, to: Date()) newItem.moodValue = Int16(Mood.allValues.randomElement()!.rawValue) - newItem.date = Calendar.current.date(byAdding: .day, value: -idx, to: Date()) + + 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() @@ -95,15 +129,15 @@ struct PersistenceController { func fillInMissingDates() { let fetchRequest = NSFetchRequest(entityName: "MoodEntry") - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)] + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: false)] let entries = try! viewContext.fetch(fetchRequest) - if let earliestDate = entries.last?.date { + if let earliestDate = entries.last?.forDate { let diffInDays = Calendar.current.dateComponents([.day], from: earliestDate, to: Date()).day for idx in 1.. Int { + let num = data.filter({ + $0.moodValue == moodType.rawValue + }).count + return num + } +} diff --git a/Shared/views/AddMoodHeaderView.swift b/Shared/views/AddMoodHeaderView.swift index 1704355..89dba7f 100644 --- a/Shared/views/AddMoodHeaderView.swift +++ b/Shared/views/AddMoodHeaderView.swift @@ -23,13 +23,15 @@ struct AddMoodHeaderView: View { .foregroundColor(Color(UIColor.label)) .padding() HStack{ - ForEach(Mood.allValues) { mood in + ForEach(Mood.allValues.reversed()) { mood in VStack { Button(action: { addItem(withMoodValue: mood.rawValue) }, label: { mood.icon - .font(.system(size: 50)) + .resizable() + .frame(width: 50, height: 50, alignment: .center) + .foregroundColor(mood.color) }) //Text(mood.strValue) @@ -50,7 +52,7 @@ struct AddMoodHeaderView: View { let newItem = MoodEntry(context: viewContext) newItem.timestamp = Date() newItem.moodValue = Int16(moodValue) - newItem.date = Date() + newItem.forDate = Date() do { try viewContext.save() diff --git a/Shared/views/CircleView.swift b/Shared/views/CircleView.swift new file mode 100644 index 0000000..a2f3d75 --- /dev/null +++ b/Shared/views/CircleView.swift @@ -0,0 +1,41 @@ +// +// CircleView.swift +// Feels (iOS) +// +// Created by Trey Tartt on 1/13/22. +// + +import Foundation +import SwiftUI + +struct DayChartView: View, Hashable { + + enum ViewType: Hashable { + case cicle + case square + case text(String) + } + + let color: Color + let weekDay: Int + let viewType: ViewType + + var body: some View { + switch viewType { + case .cicle: + Circle() + .fill(color) + .frame(minWidth: 5, idealWidth: 50, maxWidth: 50, minHeight: 5, idealHeight: 20, maxHeight: 50, alignment: .center) + .opacity(color == Mood.missing.color ? 0.5 : 1.0) + case .square: + Rectangle() + .fill(color) + .frame(minWidth: 5, idealWidth: 50, maxWidth: 50, minHeight: 5, idealHeight: 20, maxHeight: 50, alignment: .center) + case .text(let value): + Text(value) + .font(.footnote) + .frame(minWidth: 5, idealWidth: 50, maxWidth: 50, minHeight: 5, idealHeight: 20, maxHeight: 50, alignment: .center) + } + + } +} diff --git a/Shared/views/ContentView.swift b/Shared/views/ContentView.swift index 9fa94a8..89d9eba 100644 --- a/Shared/views/ContentView.swift +++ b/Shared/views/ContentView.swift @@ -16,7 +16,7 @@ struct ContentView: View { @State private var showTodayInput = true @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \MoodEntry.date, ascending: false)], + sortDescriptors: [NSSortDescriptor(keyPath: \MoodEntry.forDate, ascending: false)], animation: .spring()) private var items: FetchedResults @@ -29,10 +29,16 @@ struct ContentView: View { Label("Main", systemImage: "list.dash") } + FilterView() + .tabItem { + Label("Filter", systemImage: "calendar.circle") + } + GraphView() .tabItem { - Label("Graph", systemImage: "chart.line.uptrend.xyaxis") + Label("Stats", systemImage: "chart.line.uptrend.xyaxis") } + } } @@ -56,13 +62,15 @@ struct ContentView: View { ForEach(items) { item in HStack { item.mood.icon - .font(.system(size: 50)) + .resizable() + .frame(width: 50, height: 50, alignment: .center) + .foregroundColor(item.mood.color) VStack { Text("\(item.moodString)") .font(.title) .foregroundColor(Color(UIColor.systemGray)) .frame(maxWidth: .infinity, alignment: .leading) - Text(item.date!, style: .date) + Text(item.forDate ?? Date(), style: .date) .font(.body) .foregroundColor(Color(UIColor.label)) .frame(maxWidth: .infinity, alignment: .leading) @@ -123,8 +131,8 @@ struct ContentView: View { // Note: Times are printed in UTC. Depending on where you live it won't print 00:00:00 but it will work with UTC times which can be converted to local time // Set predicate as date being today's date - let fromPredicate = NSPredicate(format: "%@ <= %K", dateFrom as NSDate, #keyPath(MoodEntry.date)) - let toPredicate = NSPredicate(format: "%K < %@", #keyPath(MoodEntry.date), dateTo as NSDate) + let fromPredicate = NSPredicate(format: "%@ <= %K", dateFrom as NSDate, #keyPath(MoodEntry.forDate)) + let toPredicate = NSPredicate(format: "%K < %@", #keyPath(MoodEntry.forDate), dateTo as NSDate) let datePredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fromPredicate, toPredicate]) fetchRequest.predicate = datePredicate let entries = try! self.viewContext.count(for: fetchRequest) diff --git a/Shared/views/FilterView.swift b/Shared/views/FilterView.swift new file mode 100644 index 0000000..e6d8023 --- /dev/null +++ b/Shared/views/FilterView.swift @@ -0,0 +1,362 @@ +// +// FilterView.swift +// Feels +// +// Created by Trey Tartt on 1/12/22. +// + +import SwiftUI +import CoreData + +class DataHolder: ObservableObject { + // year, month, items + @Published var data = [Int: [Int: [DayChartView]]]() + @Published var numberOfRatings: Int = 0 + var uncategorizedData = [MoodEntry]() { + didSet { + self.numberOfRatings = uncategorizedData.count + } + } +} + + +struct FilterView: View { + typealias Year = Int + typealias Month = Int + + let weekdays = [("Sun", 1), ("mon", 2), ("tue", 3), ("wed", 4), ("thur", 5), ("fri", 6), ("sat", 7)] + 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 var selectedDays = [Int]() + @State private var entryStartDate: Date = Date() + @State private var entryEndDate: Date = Date() + @State private var showFilter = false + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \MoodEntry.forDate, ascending: false)], + animation: .spring()) + private var items: FetchedResults + + @StateObject private var dataHolder = DataHolder() + //[ + // 2001: [0: [], 1: [], 2: []], + // 2002: [0: [], 1: [], 2: []] + // ] + let columns = [ + GridItem(.flexible(minimum: 5, maximum: 50)), + GridItem(.flexible(minimum: 5, maximum: 50)), + GridItem(.flexible(minimum: 5, maximum: 50)), + GridItem(.flexible(minimum: 5, maximum: 50)), + GridItem(.flexible(minimum: 5, maximum: 50)), + GridItem(.flexible(minimum: 5, maximum: 50)), + GridItem(.flexible(minimum: 5, maximum: 50)), + GridItem(.flexible(minimum: 5, maximum: 50)), + GridItem(.flexible(minimum: 5, maximum: 50)), + GridItem(.flexible(minimum: 5, maximum: 50)), + GridItem(.flexible(minimum: 5, maximum: 50)), + GridItem(.flexible(minimum: 5, maximum: 50)), + ] + + private func filterEntries(startDate: Date, endDate: Date) { + let filteredEntries = PersistenceController.shared.getData(startDate: startDate, endDate: endDate) + self.dataHolder.data.removeAll() + let filledOutData = buildGridData(withData: filteredEntries) + self.dataHolder.data = filledOutData + self.dataHolder.uncategorizedData = filteredEntries + } + + private func buildGridData(withData data: [MoodEntry]) -> [Year: [Month: [DayChartView]]] { + var returnData = [Year: [Month: [DayChartView]]]() + + 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: [DayChartView]]() + + // add back in if months header has leading (-1, ""), + // and add back gridItem +// var dayViews = [DayChartView]() +// for day in 0...32 { +// let view = DayChartView(color: Mood.missing.color, +// weekDay: 2, +// viewType: .text(String(day+1))) +// dayViews.append(view) +// } +// allMonths[0] = dayViews + + 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) + }) + + allMonths[month] = createViewFor(monthEntries: items, forMonth: startDateOfMonth) + } + returnData[year] = allMonths + } + } + return returnData + } + + private func createViewFor(monthEntries: [MoodEntry], forMonth month: Date) -> [DayChartView] { + var filledOutArray = [DayChartView]() + + let calendar = Calendar.current + let range = calendar.range(of: .day, in: .month, for: month)! + let numDays = range.count + + for day in 1...numDays { + if let item = monthEntries.filter({ entry in + let components = calendar.dateComponents([.day, .weekday], from: entry.forDate!) + let date = components.day + let weekday = components.weekday! + + if selectedDays.isEmpty { + return day == date + } else { + return day == date && selectedDays.contains(weekday) + } + }).first { + let view = DayChartView(color: item.mood.color, + weekDay: Int(item.weekDay), + viewType: .cicle) + filledOutArray.append(view) + } else { + let thisDate = Calendar.current.date(bySetting: .day, value: day, of: month)! + let view = DayChartView(color: Mood.missing.color, + weekDay: Calendar.current.component(.weekday, from: thisDate), + viewType: .cicle) + filledOutArray.append(view) + } + } + + for _ in filledOutArray.count...32 { + let view = DayChartView(color: Mood.missing.color, + weekDay: 2, + viewType: .cicle) + filledOutArray.append(view) + } + + return filledOutArray + } + + var body: some View { + VStack { + Button(action: { + withAnimation{ + showFilter.toggle() + } + }, label: { + Text("filter") + .textCase(.uppercase) + }) + .padding([.leading, .trailing, .top]) + + statsView + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 120, maxHeight: 120) + .cornerRadius(25) + .padding([.leading, .trailing]) + + if showFilter { + filterView + } + + gridView + .onAppear(perform: { + let monthEntries = PersistenceController.shared.getData(startDate: Date(timeIntervalSince1970: 0), endDate: Date()) + entryStartDate = monthEntries.first!.forDate! + entryEndDate = monthEntries.last!.forDate! + self.dataHolder.data = buildGridData(withData: monthEntries) + self.dataHolder.uncategorizedData = monthEntries + // filterEntries(startDate: entryStartDate, endDate: entryEndDate) + }) + } + } + + struct StatsSubView: View { + let data: [MoodEntry] + let mood: Mood + + var body: some View { + VStack { + Text(String(Stats.getCountFor(moodType: mood, + inData: data))) + .font(.title) + Text(mood.strValue) + .foregroundColor(mood.color) + } + } + } + + private var statsView: some View { + ZStack { + Color(UIColor.secondarySystemBackground) + + HStack { + Spacer() + ForEach(Mood.allValues.reversed(), id: \.self) { mood in + StatsSubView(data: self.dataHolder.uncategorizedData, mood: mood) + Spacer() + } + } + } + .cornerRadius(25) + .padding() + } + + private var filterView: some View { + VStack { + VStack { + ZStack { + Color(UIColor.secondarySystemBackground) + DatePicker( + "Start Date", + selection: $entryStartDate, + displayedComponents: [.date] + ).onChange(of: entryStartDate, perform: { value in + filterEntries(startDate: self.entryStartDate, endDate: self.entryEndDate) + }) + .padding() + } + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44) + .cornerRadius(25) + .padding([.leading, .trailing]) + + ZStack { + Color(UIColor.secondarySystemBackground) + DatePicker( + "End Date", + selection: $entryEndDate, + displayedComponents: [.date] + ).onChange(of: entryStartDate, perform: { value in + filterEntries(startDate: self.entryStartDate, endDate: self.entryEndDate) + }) + .padding() + } + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44) + .cornerRadius(25) + .padding([.leading, .trailing]) + + ZStack { + Color(UIColor.secondarySystemBackground) + HStack { + Spacer() + ForEach(weekdays.indices, id: \.self) { dayIdx in + let day = String(weekdays[dayIdx].0) + let value = weekdays[dayIdx].1 + + Button(day.capitalized, action: { + if let index = selectedDays.firstIndex(of: value) { + selectedDays.remove(at: index) + } else { + selectedDays.append(value) + } + filterEntries(startDate: entryStartDate, endDate: entryEndDate) + }) + .frame(maxWidth: .infinity) + .foregroundColor(selectedDays.contains(value) || selectedDays.isEmpty ? .green : .red) + } + Spacer() + } + } + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44) + .cornerRadius(25) + .padding([ .leading, .trailing]) + } + } + } + + private var monthsHeader: some View { + LazyVGrid(columns: columns, spacing: 0) { + ForEach(months, id: \.self.0) { item in + Text(item.1) + .textCase(.uppercase) + } + }.padding([.leading, .trailing, .top]) + } + + private var gridView: some View { + VStack { + Text("Total: \(self.dataHolder.numberOfRatings)") + .font(.title2) + + monthsHeader + .cornerRadius(25) + .padding([.leading, .trailing]) + + VStack { + ScrollView { + ForEach(Array(self.dataHolder.data.keys.sorted(by: >)), id: \.self) { yearKey in + let yearData = self.dataHolder.data[yearKey]! + Text(String(yearKey)) + .font(.title) + yearGridView(yearData: yearData, columns: columns) + } + } + .padding() + } + } + } + + private struct yearGridView: View { + let yearData: [Int: [DayChartView]] + let columns: [GridItem] + + var body: some View { + ZStack { + Color(UIColor.secondarySystemBackground) + + VStack { + LazyVGrid(columns: columns, spacing: 0) { + ForEach(Array(yearData.keys.sorted(by: <)), id: \.self) { monthKey in + let monthData = yearData[monthKey]! + VStack { + monthGridView(monthData: monthData) + } + } + } + .padding([.leading, .trailing, .top, .bottom]) + } + } + .cornerRadius(25) + } + } + + private struct monthGridView: View { + let monthData: [DayChartView] + + var body: some View { + VStack { + ForEach(monthData, id: \.self) { view in + view + } + } + } + } +} + +struct FilterView_Previews: PreviewProvider { + static var previews: some View { + Group { + FilterView() + + FilterView() + .preferredColorScheme(.dark) + } + } +} diff --git a/Shared/views/GraphView.swift b/Shared/views/GraphView.swift index 894ef0e..1cdfcf3 100644 --- a/Shared/views/GraphView.swift +++ b/Shared/views/GraphView.swift @@ -8,9 +8,137 @@ import Foundation import SwiftUI import CoreData +import Charts struct GraphView: View { var body: some View { - Text("this is a graph") + ZStack { + Color(UIColor.secondarySystemBackground) + VStack { + VStack { + HStack { + ZStack { + Color(UIColor.systemBackground) + BarChartGraph(entries: [ + BarChartDataEntry(x: 1, y: Double(Int.random(in: 0...10))), + BarChartDataEntry(x: 2, y: Double(Int.random(in: 0...10))), + BarChartDataEntry(x: 3, y: Double(Int.random(in: 0...10))), + BarChartDataEntry(x: 4, y: Double(Int.random(in: 0...10))), + BarChartDataEntry(x: 5, y: Double(Int.random(in: 0...10))) + ]) + } + .clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous)) + .padding() + + ZStack { + Color(UIColor.systemBackground) + BarChartGraph(entries: [ + BarChartDataEntry(x: 1, y: Double(Int.random(in: 0...10))), + BarChartDataEntry(x: 2, y: Double(Int.random(in: 0...10))), + BarChartDataEntry(x: 3, y: Double(Int.random(in: 0...10))), + BarChartDataEntry(x: 4, y: Double(Int.random(in: 0...10))), + BarChartDataEntry(x: 5, y: Double(Int.random(in: 0...10))) + ]) + } + .clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous)) + .padding() + } + + ZStack { + Color(UIColor.systemBackground) + BarChartGraph(entries: [ + BarChartDataEntry(x: 1, y: Double(Int.random(in: 0...10))), + BarChartDataEntry(x: 2, y: Double(Int.random(in: 0...10))), + BarChartDataEntry(x: 3, y: Double(Int.random(in: 0...10))), + BarChartDataEntry(x: 4, y: Double(Int.random(in: 0...10))), + BarChartDataEntry(x: 5, y: Double(Int.random(in: 0...10))) + ]) + }.clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous)) + .padding() + + ZStack { + Color(UIColor.systemBackground) + BarChartGraph(entries: [ + BarChartDataEntry(x: 1, y: Double(Int.random(in: 0...10))), + BarChartDataEntry(x: 2, y: Double(Int.random(in: 0...10))), + BarChartDataEntry(x: 3, y: Double(Int.random(in: 0...10))), + BarChartDataEntry(x: 4, y: Double(Int.random(in: 0...10))), + BarChartDataEntry(x: 5, y: Double(Int.random(in: 0...10))) + ]) + }.clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous)) + .padding() + } + } + } + } +} + +struct BarChartGraph: UIViewRepresentable { + //Bar chart accepts data as array of BarChartDataEntry objects + var entries : [BarChartDataEntry] + + // this func is required to conform to UIViewRepresentable protocol + func makeUIView(context: Context) -> BarChartView { + //crate new chart + let chart = BarChartView() + chart.drawGridBackgroundEnabled = false + chart.drawValueAboveBarEnabled = false + + chart.xAxis.drawAxisLineEnabled = false + chart.xAxis.labelTextColor = .clear + + chart.rightAxis.drawAxisLineEnabled = false + chart.rightAxis.labelTextColor = .clear + + chart.leftAxis.drawAxisLineEnabled = false + chart.leftAxis.labelTextColor = .clear + + chart.xAxis.drawGridLinesEnabled = false + chart.leftAxis.drawGridLinesEnabled = false + chart.leftAxis.axisLineColor = .clear + chart.rightAxis.axisLineColor = .clear + + chart.legend.textColor = .clear + chart.legend.enabled = false + + chart.drawBordersEnabled = false + chart.drawMarkers = false + // chart.yAxis.drawGridLinesEnabled = false + chart.rightAxis.drawGridLinesEnabled = false + chart.borderColor = .clear + //it is convenient to form chart data in a separate func + chart.data = addData() + return chart + } + + // this func is required to conform to UIViewRepresentable protocol + func updateUIView(_ uiView: BarChartView, context: Context) { + //when data changes chartd.data update is required + uiView.data = addData() + } + + func addData() -> BarChartData{ + let data = BarChartData() + //BarChartDataSet is an object that contains information about your data, styling and more + let dataSet = BarChartDataSet(entries: entries) + // change bars color to green + dataSet.colors = [NSUIColor.green] + //change data label + data.append(dataSet) + return data + } + + typealias UIViewType = BarChartView +} + + +struct GraphView_Previews: PreviewProvider { + static var previews: some View { + Group { + GraphView() + + GraphView() + .preferredColorScheme(.dark) + } } } diff --git a/Shared/views/HeaderStatsView.swift b/Shared/views/HeaderStatsView.swift index e449b0f..7734ff1 100644 --- a/Shared/views/HeaderStatsView.swift +++ b/Shared/views/HeaderStatsView.swift @@ -58,7 +58,7 @@ struct HeaderStatsView : UIViewRepresentable { // change bars color to green dataSet.colors = [NSUIColor.green] //change data label - data.addDataSet(dataSet) + data.append(dataSet) return data } diff --git a/Shared/views/SettingsView.swift b/Shared/views/SettingsView.swift index cf23c91..e7668f9 100644 --- a/Shared/views/SettingsView.swift +++ b/Shared/views/SettingsView.swift @@ -10,47 +10,32 @@ import SwiftUI struct SettingsView: View { @Environment(\.dismiss) var dismiss - @State private var currentDate = Date() { + @AppStorage("notificationDate") private var notificationDate = Date() { didSet { if self.showReminder { - LocalNotification.scheduleReminder(atTime: self.currentDate) + LocalNotification.scheduleReminder(atTime: self.notificationDate) } } } - @State private var showReminder: Bool = false { - didSet { - if self.showReminder { - LocalNotification.testIfEnabled(completion: { result in - switch result{ - case .success(_): - LocalNotification.scheduleReminder(atTime: self.currentDate) - case .failure(_): - // show error - break - } - }) - } else { - LocalNotification.removeNotificaiton() - } - } - } + @AppStorage("showReminder") private var showReminder: Bool = false - var body: some View { ZStack { - Color(UIColor.secondarySystemBackground) - - VStack { - closeButtonView - .padding() - notificationCell - randomShitCell - addTestDataCell - clearDB - whyBackgroundMode - Spacer() + var body: some View { + ZStack { + Color(UIColor.secondarySystemBackground) + + VStack { + closeButtonView + .padding() + notificationCell + randomShitCell + addTestDataCell + clearDB + whyBackgroundMode + Spacer() + } + .padding() } - .padding() - } } private var closeButtonView: some View { @@ -71,8 +56,14 @@ struct SettingsView: View { Color(UIColor.systemBackground) VStack { Toggle("Would you like to be reminded?", isOn: $showReminder) + .onChange(of: showReminder, perform: { value in + self.maybeNotificaiotns(areEnabled: value) + }) .padding() - DatePicker("", selection: $currentDate, displayedComponents: .hourAndMinute) + DatePicker("", selection: $notificationDate, displayedComponents: .hourAndMinute) + .onChange(of: notificationDate, perform: { value in + self.updateNotificationTimes(toDate: value) + }) .disabled(showReminder == false) .padding() } @@ -85,7 +76,11 @@ struct SettingsView: View { ZStack { Color(UIColor.systemBackground) VStack { - Text("random shit") + Button(action: { + + }, label: { + Text("Special thanks to") + }) .padding() } } @@ -130,6 +125,29 @@ struct SettingsView: View { .fixedSize(horizontal: false, vertical: true) .clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous)) } + + private func updateNotificationTimes(toDate date: Date) { + LocalNotification.removeNotificaiton() + + LocalNotification.scheduleReminder(atTime: date) + } + + private func maybeNotificaiotns(areEnabled: Bool) { + if areEnabled { + LocalNotification.testIfEnabled(completion: { result in + switch result{ + case .success(_): + LocalNotification.scheduleReminder(atTime: self.notificationDate) + case .failure(let error): + print(error) + // show error + break + } + }) + } else { + LocalNotification.removeNotificaiton() + } + } } struct SettingsView_Previews: PreviewProvider {