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 0000000..f2e0326 Binary files /dev/null and b/Shared/Assets.xcassets/missing.imageset/Screen Shot 2022-01-13 at 5.02.34 PM.png differ diff --git a/Shared/BGTask.swift b/Shared/BGTask.swift new file mode 100644 index 0000000..ea40a66 --- /dev/null +++ b/Shared/BGTask.swift @@ -0,0 +1,40 @@ +// +// BGTask.swift +// Feels (iOS) +// +// Created by Trey Tartt on 1/12/22. +// + +import Foundation +import BackgroundTasks + +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/Feels.xcdatamodeld/Shared.xcdatamodel/contents b/Shared/Feels.xcdatamodeld/Shared.xcdatamodel/contents index 432945a..652bae8 100644 --- a/Shared/Feels.xcdatamodeld/Shared.xcdatamodel/contents +++ b/Shared/Feels.xcdatamodeld/Shared.xcdatamodel/contents @@ -1,11 +1,12 @@ - + + - + \ 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 {