diff --git a/.github/.keep b/.github/.keep old mode 100644 new mode 100755 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml old mode 100644 new mode 100755 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 1acd39b..2f6b4c3 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,8 @@ ## Build generated build/ DerivedData/ -#Package.resolved +ResearchKit/ +ParseCareKit-heroku.plist ## Various settings .DS_Store diff --git a/.gitmodules b/.gitmodules new file mode 100755 index 0000000..d50f0f5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ResearchKit"] + path = ResearchKit + url = https://github.com/netreconlab/ResearchKit.git diff --git a/.swiftlint.yml b/.swiftlint.yml old mode 100644 new mode 100755 index 0e7d563..86b9c34 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,6 +1,8 @@ excluded: # paths to ignore during linting. Takes precedence over `included`. - DerivedData + - ResearchKit disabled_rules: + - multiple_closures_with_trailing_closure - weak_delegate - file_length - function_body_length diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md old mode 100644 new mode 100755 diff --git a/OCKSample.xcodeproj/project.pbxproj b/OCKSample.xcodeproj/project.pbxproj old mode 100644 new mode 100755 index c128e06..acfa48f --- a/OCKSample.xcodeproj/project.pbxproj +++ b/OCKSample.xcodeproj/project.pbxproj @@ -103,6 +103,59 @@ E7440E4F229477F7007AD30A /* CareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7440E4E229477F7007AD30A /* CareViewController.swift */; }; E7C37849228F887800E982D8 /* TipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7C37848228F887800E982D8 /* TipView.swift */; }; E7C4CA0B22809AD500ECC3D7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E7C4CA0A22809AD500ECC3D7 /* Assets.xcassets */; }; + F5144AED292C452200DA41A6 /* CustomContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5144AEC292C452200DA41A6 /* CustomContactViewController.swift */; }; + F5144AF1292C488A00DA41A6 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5144AF0292C488A00DA41A6 /* ImagePicker.swift */; }; + F5144AF3292C489800DA41A6 /* ProfileImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5144AF2292C489800DA41A6 /* ProfileImageView.swift */; }; + F5144AF5292C48F300DA41A6 /* OCKBiologicalSex+Hashable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5144AF4292C48F300DA41A6 /* OCKBiologicalSex+Hashable.swift */; }; + F5144AF9292C4B2000DA41A6 /* MyContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5144AF8292C4B2000DA41A6 /* MyContactView.swift */; }; + F5144AFB292C4B2300DA41A6 /* MyContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5144AFA292C4B2300DA41A6 /* MyContactViewController.swift */; }; + F5144AFF292C71FD00DA41A6 /* OCKSynchronizedStoreManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5144AFE292C71FD00DA41A6 /* OCKSynchronizedStoreManager.swift */; }; + F5144B00292C749700DA41A6 /* OCKSynchronizedStoreManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5144AFE292C71FD00DA41A6 /* OCKSynchronizedStoreManager.swift */; }; + F52CF9AA28F9E398004EEBF0 /* AnimationStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F52CF9A928F9E398004EEBF0 /* AnimationStyle.swift */; }; + F52CF9AC28F9E4BF004EEBF0 /* AppearanceStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F52CF9AB28F9E4BF004EEBF0 /* AppearanceStyle.swift */; }; + F52CF9AE28F9E4E9004EEBF0 /* DimensionStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F52CF9AD28F9E4E9004EEBF0 /* DimensionStyle.swift */; }; + F53ED971292D759F002C3413 /* OCKTask+Custom.swift in Sources */ = {isa = PBXBuildFile; fileRef = F583B697290B56BD00FE1C5A /* OCKTask+Custom.swift */; }; + F53ED977292D76A0002C3413 /* Consent.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53ED975292D76A0002C3413 /* Consent.swift */; }; + F53ED97B292D7C66002C3413 /* OCKAnyEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53ED97A292D7C66002C3413 /* OCKAnyEvent.swift */; }; + F53ED97C292D7C66002C3413 /* OCKAnyEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53ED97A292D7C66002C3413 /* OCKAnyEvent.swift */; }; + F53ED980292D7D9B002C3413 /* SurveyViewSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53ED97F292D7D9B002C3413 /* SurveyViewSynchronizer.swift */; }; + F53ED984292D7E86002C3413 /* Surveyable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53ED982292D7E85002C3413 /* Surveyable.swift */; }; + F53ED985292D7E86002C3413 /* Surveyable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53ED982292D7E85002C3413 /* Surveyable.swift */; }; + F53ED986292D7E86002C3413 /* Survey.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53ED983292D7E86002C3413 /* Survey.swift */; }; + F53ED987292D7E86002C3413 /* Survey.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53ED983292D7E86002C3413 /* Survey.swift */; }; + F53ED98B292D7E8F002C3413 /* Onboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53ED988292D7E8E002C3413 /* Onboard.swift */; }; + F53ED98C292D7E8F002C3413 /* RangeOfMotion.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53ED989292D7E8F002C3413 /* RangeOfMotion.swift */; }; + F53ED98D292D7E8F002C3413 /* CheckIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53ED98A292D7E8F002C3413 /* CheckIn.swift */; }; + F53ED98E292D8120002C3413 /* CheckIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53ED98A292D7E8F002C3413 /* CheckIn.swift */; }; + F53ED98F292D8123002C3413 /* Onboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53ED988292D7E8E002C3413 /* Onboard.swift */; }; + F53ED990292D8124002C3413 /* RangeOfMotion.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53ED989292D7E8F002C3413 /* RangeOfMotion.swift */; }; + F545000A294009DB009A994F /* OCKAnyOutcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5450009294009DB009A994F /* OCKAnyOutcome.swift */; }; + F545000B294009DB009A994F /* OCKAnyOutcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5450009294009DB009A994F /* OCKAnyOutcome.swift */; }; + F545000D294009F4009A994F /* OCKTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F545000C294009F4009A994F /* OCKTaskController.swift */; }; + F545000E294009F4009A994F /* OCKTaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F545000C294009F4009A994F /* OCKTaskController.swift */; }; + F5450010294009FB009A994F /* OCKTaskEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = F545000F294009FB009A994F /* OCKTaskEvents.swift */; }; + F5450011294009FB009A994F /* OCKTaskEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = F545000F294009FB009A994F /* OCKTaskEvents.swift */; }; + F545001329400A87009A994F /* CardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F545001229400A86009A994F /* CardViewModel.swift */; }; + F545001429400A87009A994F /* CardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F545001229400A86009A994F /* CardViewModel.swift */; }; + F545001829400AD4009A994F /* CustomCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F545001629400AD4009A994F /* CustomCardView.swift */; }; + F545001929400AD4009A994F /* CustomCardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F545001729400AD4009A994F /* CustomCardViewModel.swift */; }; + F545001B29400B02009A994F /* ScheduleUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = F545001A29400B02009A994F /* ScheduleUtility.swift */; }; + F545001C29400B02009A994F /* ScheduleUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = F545001A29400B02009A994F /* ScheduleUtility.swift */; }; + F545001E29400F96009A994F /* OCKEventAggregator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F545001D29400F96009A994F /* OCKEventAggregator.swift */; }; + F545001F29400F96009A994F /* OCKEventAggregator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F545001D29400F96009A994F /* OCKEventAggregator.swift */; }; + F545002229400FFD009A994F /* InsightsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F545002129400FFD009A994F /* InsightsView.swift */; }; + F54500242940100E009A994F /* InsightsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F54500232940100E009A994F /* InsightsViewController.swift */; }; + F583B691290B1C5100FE1C5A /* TaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F583B690290B1C5100FE1C5A /* TaskView.swift */; }; + F583B693290B1C8F00FE1C5A /* TaskViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F583B692290B1C8F00FE1C5A /* TaskViewModel.swift */; }; + F583B698290B56BD00FE1C5A /* OCKTask+Custom.swift in Sources */ = {isa = PBXBuildFile; fileRef = F583B697290B56BD00FE1C5A /* OCKTask+Custom.swift */; }; + F583B69A290B612400FE1C5A /* HealthKitTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F583B699290B612400FE1C5A /* HealthKitTaskView.swift */; }; + F583B69C290B619500FE1C5A /* HealthKitTaskViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F583B69B290B619500FE1C5A /* HealthKitTaskViewModel.swift */; }; + F5ADA23E28F9FA4600D5317F /* AppearanceStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F52CF9AB28F9E4BF004EEBF0 /* AppearanceStyle.swift */; }; + F5ADA23F28F9FA4900D5317F /* DimensionStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F52CF9AD28F9E4E9004EEBF0 /* DimensionStyle.swift */; }; + F5ADA24028F9FA4B00D5317F /* AnimationStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F52CF9A928F9E398004EEBF0 /* AnimationStyle.swift */; }; + F5BE45A82936AD0B008D3CCE /* CustomFeaturedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BE45A72936AD0B008D3CCE /* CustomFeaturedContentView.swift */; }; + F5F43E1B2936A6E900D1A16F /* ResearchKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F53ED96C292D7000002C3413 /* ResearchKit.framework */; }; + F5F43E1C2936A6E900D1A16F /* ResearchKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F53ED96C292D7000002C3413 /* ResearchKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -120,28 +173,43 @@ remoteGlobalIDString = 91AD922624A4C42B00925D4D; remoteInfo = OCKWatchSample; }; + F53ED96B292D7000002C3413 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F53ED965292D7000002C3413 /* ResearchKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = B183A5951A8535D100C76870; + remoteInfo = ResearchKit; + }; + F53ED96D292D7000002C3413 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F53ED965292D7000002C3413 /* ResearchKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 86CC8E9A1AC09332001CCD89; + remoteInfo = ResearchKitTests; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ - 51431FA223D166AE006A7CFD /* Embed Frameworks */ = { + 91AD924C24A4C42E00925D4D /* Embed Watch Content */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; + dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; + dstSubfolderSpec = 16; files = ( + 91AD924B24A4C42E00925D4D /* OCKWatchSample.app in Embed Watch Content */, ); - name = "Embed Frameworks"; + name = "Embed Watch Content"; runOnlyForDeploymentPostprocessing = 0; }; - 91AD924C24A4C42E00925D4D /* Embed Watch Content */ = { + F5F43E1D2936A6E900D1A16F /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; - dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; - dstSubfolderSpec = 16; + dstPath = ""; + dstSubfolderSpec = 10; files = ( - 91AD924B24A4C42E00925D4D /* OCKWatchSample.app in Embed Watch Content */, + F5F43E1C2936A6E900D1A16F /* ResearchKit.framework in Embed Frameworks */, ); - name = "Embed Watch Content"; + name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ @@ -220,6 +288,41 @@ E7440E4E229477F7007AD30A /* CareViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CareViewController.swift; sourceTree = ""; }; E7C37848228F887800E982D8 /* TipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipView.swift; sourceTree = ""; }; E7C4CA0A22809AD500ECC3D7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + F5144AEC292C452200DA41A6 /* CustomContactViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomContactViewController.swift; sourceTree = ""; }; + F5144AF0292C488A00DA41A6 /* ImagePicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; + F5144AF2292C489800DA41A6 /* ProfileImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileImageView.swift; sourceTree = ""; }; + F5144AF4292C48F300DA41A6 /* OCKBiologicalSex+Hashable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OCKBiologicalSex+Hashable.swift"; sourceTree = ""; }; + F5144AF8292C4B2000DA41A6 /* MyContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyContactView.swift; sourceTree = ""; }; + F5144AFA292C4B2300DA41A6 /* MyContactViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyContactViewController.swift; sourceTree = ""; }; + F5144AFE292C71FD00DA41A6 /* OCKSynchronizedStoreManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKSynchronizedStoreManager.swift; sourceTree = ""; }; + F52CF9A928F9E398004EEBF0 /* AnimationStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationStyle.swift; sourceTree = ""; }; + F52CF9AB28F9E4BF004EEBF0 /* AppearanceStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceStyle.swift; sourceTree = ""; }; + F52CF9AD28F9E4E9004EEBF0 /* DimensionStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DimensionStyle.swift; sourceTree = ""; }; + F53ED965292D7000002C3413 /* ResearchKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ResearchKit.xcodeproj; path = ResearchKit/ResearchKit.xcodeproj; sourceTree = ""; }; + F53ED975292D76A0002C3413 /* Consent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Consent.swift; sourceTree = ""; }; + F53ED97A292D7C66002C3413 /* OCKAnyEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKAnyEvent.swift; sourceTree = ""; }; + F53ED97F292D7D9B002C3413 /* SurveyViewSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurveyViewSynchronizer.swift; sourceTree = ""; }; + F53ED982292D7E85002C3413 /* Surveyable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Surveyable.swift; sourceTree = ""; }; + F53ED983292D7E86002C3413 /* Survey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Survey.swift; sourceTree = ""; }; + F53ED988292D7E8E002C3413 /* Onboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Onboard.swift; sourceTree = ""; }; + F53ED989292D7E8F002C3413 /* RangeOfMotion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RangeOfMotion.swift; sourceTree = ""; }; + F53ED98A292D7E8F002C3413 /* CheckIn.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckIn.swift; sourceTree = ""; }; + F5450009294009DB009A994F /* OCKAnyOutcome.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKAnyOutcome.swift; sourceTree = ""; }; + F545000C294009F4009A994F /* OCKTaskController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKTaskController.swift; sourceTree = ""; }; + F545000F294009FB009A994F /* OCKTaskEvents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKTaskEvents.swift; sourceTree = ""; }; + F545001229400A86009A994F /* CardViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardViewModel.swift; sourceTree = ""; }; + F545001629400AD4009A994F /* CustomCardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomCardView.swift; sourceTree = ""; }; + F545001729400AD4009A994F /* CustomCardViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomCardViewModel.swift; sourceTree = ""; }; + F545001A29400B02009A994F /* ScheduleUtility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScheduleUtility.swift; sourceTree = ""; }; + F545001D29400F96009A994F /* OCKEventAggregator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKEventAggregator.swift; sourceTree = ""; }; + F545002129400FFD009A994F /* InsightsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsightsView.swift; sourceTree = ""; }; + F54500232940100E009A994F /* InsightsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsightsViewController.swift; sourceTree = ""; }; + F583B690290B1C5100FE1C5A /* TaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskView.swift; sourceTree = ""; }; + F583B692290B1C8F00FE1C5A /* TaskViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskViewModel.swift; sourceTree = ""; }; + F583B697290B56BD00FE1C5A /* OCKTask+Custom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKTask+Custom.swift"; sourceTree = ""; }; + F583B699290B612400FE1C5A /* HealthKitTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitTaskView.swift; sourceTree = ""; }; + F583B69B290B619500FE1C5A /* HealthKitTaskViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitTaskViewModel.swift; sourceTree = ""; }; + F5BE45A72936AD0B008D3CCE /* CustomFeaturedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFeaturedContentView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -229,6 +332,7 @@ files = ( 70202EC12807333900CF73FB /* CareKit in Frameworks */, 918FDEAF271B3F8F0045A0EF /* ParseCareKit in Frameworks */, + F5F43E1B2936A6E900D1A16F /* ResearchKit.framework in Frameworks */, 70202EC52807333A00CF73FB /* CareKitUI in Frameworks */, 708542F9276687F90029E888 /* HealthKit.framework in Frameworks */, 70202EC32807333900CF73FB /* CareKitFHIR in Frameworks */, @@ -274,7 +378,11 @@ 70221F2B28D7C7C600971195 /* CustomCards */ = { isa = PBXGroup; children = ( + F545001529400AB6009A994F /* CustomCard */, E7C37848228F887800E982D8 /* TipView.swift */, + F545001229400A86009A994F /* CardViewModel.swift */, + F53ED97F292D7D9B002C3413 /* SurveyViewSynchronizer.swift */, + F5BE45A72936AD0B008D3CCE /* CustomFeaturedContentView.swift */, ); path = CustomCards; sourceTree = ""; @@ -300,6 +408,9 @@ 703088772582727500FFABB6 /* Main */ = { isa = PBXGroup; children = ( + F545002029400FDC009A994F /* Insights */, + F53ED981292D7DBE002C3413 /* Surveys */, + F53ED974292D7688002C3413 /* Onboarding */, 70221F2728D7809800971195 /* MainTabView.swift */, 7036E4D2256EBE35006E9A3C /* MainView.swift */, 70308878258272CD00FFABB6 /* Care */, @@ -324,8 +435,16 @@ 7030887C258272E700FFABB6 /* Profile */ = { isa = PBXGroup; children = ( + F5144AF8292C4B2000DA41A6 /* MyContactView.swift */, + F5144AFA292C4B2300DA41A6 /* MyContactViewController.swift */, + F5144AF2292C489800DA41A6 /* ProfileImageView.swift */, + F5144AF0292C488A00DA41A6 /* ImagePicker.swift */, 7036E4C8256E1A6F006E9A3C /* ProfileView.swift */, 7036E516256F2413006E9A3C /* ProfileViewModel.swift */, + F583B690290B1C5100FE1C5A /* TaskView.swift */, + F583B692290B1C8F00FE1C5A /* TaskViewModel.swift */, + F583B699290B612400FE1C5A /* HealthKitTaskView.swift */, + F583B69B290B619500FE1C5A /* HealthKitTaskViewModel.swift */, ); path = Profile; sourceTree = ""; @@ -334,6 +453,7 @@ isa = PBXGroup; children = ( 7036E4CD256E9A0C006E9A3C /* ContactView.swift */, + F5144AEC292C452200DA41A6 /* CustomContactViewController.swift */, ); path = Contact; sourceTree = ""; @@ -412,6 +532,9 @@ children = ( 9169381C271B650700A634ED /* ColorStyler.swift */, 9169381A271B64E100A634ED /* Styler.swift */, + F52CF9AD28F9E4E9004EEBF0 /* DimensionStyle.swift */, + F52CF9AB28F9E4BF004EEBF0 /* AppearanceStyle.swift */, + F52CF9A928F9E398004EEBF0 /* AnimationStyle.swift */, ); path = Stylers; sourceTree = ""; @@ -420,6 +543,7 @@ isa = PBXGroup; children = ( 918FDEB4271B40590045A0EF /* Installation.swift */, + F545001A29400B02009A994F /* ScheduleUtility.swift */, 70077596252228E900EC0EDA /* User.swift */, ); path = Models; @@ -432,14 +556,22 @@ 70221F3328D8ABBE00971195 /* AppDelegate+UIApplicationDelegate.swift */, 918FDEBB271B4E4A0045A0EF /* Calendar+Dates.swift */, 918FDEB6271B41FF0045A0EF /* Logger.swift */, + F53ED97A292D7C66002C3413 /* OCKAnyEvent.swift */, + F5450009294009DB009A994F /* OCKAnyOutcome.swift */, 70F03A922786087800E5AFB4 /* OCKAnyEvent+CustomStringConvertable.swift */, + F5144AF4292C48F300DA41A6 /* OCKBiologicalSex+Hashable.swift */, 70F03A942786093B00E5AFB4 /* OCKHealthKitPassthroughStore.swift */, 70F03AA027860AB700E5AFB4 /* OCKOutcome.swift */, 70F03A9E27860A8800E5AFB4 /* OCKOutcomeValue+Identifiable.swift */, 70F03AA227860AFF00E5AFB4 /* OCKPatient+Parse.swift */, + F5144AFE292C71FD00DA41A6 /* OCKSynchronizedStoreManager.swift */, 70F03A962786098F00E5AFB4 /* OCKStore.swift */, 70F03A9827860A0800E5AFB4 /* OCKSynchronizedStoreManager+Publishers.swift */, + F545000C294009F4009A994F /* OCKTaskController.swift */, + F545000F294009FB009A994F /* OCKTaskEvents.swift */, + F583B697290B56BD00FE1C5A /* OCKTask+Custom.swift */, 7083A855279CA40A00B3832E /* PCKUtility.swift */, + F545001D29400F96009A994F /* OCKEventAggregator.swift */, ); path = Extensions; sourceTree = ""; @@ -493,6 +625,7 @@ E72B2BFD226939E3009A9438 = { isa = PBXGroup; children = ( + F53ED965292D7000002C3413 /* ResearchKit.xcodeproj */, 9169381F271B80EA00A634ED /* CONTRIBUTING.md */, 91AD922324A461D200925D4D /* README.md */, 0520468223C3C1F2004F0F36 /* OCKSample.xctestplan */, @@ -533,6 +666,53 @@ path = OCKSample; sourceTree = ""; }; + F53ED966292D7000002C3413 /* Products */ = { + isa = PBXGroup; + children = ( + F53ED96C292D7000002C3413 /* ResearchKit.framework */, + F53ED96E292D7000002C3413 /* ResearchKitTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + F53ED974292D7688002C3413 /* Onboarding */ = { + isa = PBXGroup; + children = ( + F53ED975292D76A0002C3413 /* Consent.swift */, + ); + path = Onboarding; + sourceTree = ""; + }; + F53ED981292D7DBE002C3413 /* Surveys */ = { + isa = PBXGroup; + children = ( + F53ED98A292D7E8F002C3413 /* CheckIn.swift */, + F53ED988292D7E8E002C3413 /* Onboard.swift */, + F53ED989292D7E8F002C3413 /* RangeOfMotion.swift */, + F53ED983292D7E86002C3413 /* Survey.swift */, + F53ED982292D7E85002C3413 /* Surveyable.swift */, + ); + path = Surveys; + sourceTree = ""; + }; + F545001529400AB6009A994F /* CustomCard */ = { + isa = PBXGroup; + children = ( + F545001629400AD4009A994F /* CustomCardView.swift */, + F545001729400AD4009A994F /* CustomCardViewModel.swift */, + ); + path = CustomCard; + sourceTree = ""; + }; + F545002029400FDC009A994F /* Insights */ = { + isa = PBXGroup; + children = ( + F545002129400FFD009A994F /* InsightsView.swift */, + F54500232940100E009A994F /* InsightsViewController.swift */, + ); + path = Insights; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -578,9 +758,9 @@ E72B2C02226939E3009A9438 /* Sources */, E72B2C04226939E3009A9438 /* Resources */, 51431FA123D166AE006A7CFD /* Frameworks */, - 51431FA223D166AE006A7CFD /* Embed Frameworks */, 91AD924C24A4C42E00925D4D /* Embed Watch Content */, 9169381E271B6E2100A634ED /* SwiftLint */, + F5F43E1D2936A6E900D1A16F /* Embed Frameworks */, ); buildRules = ( ); @@ -636,6 +816,12 @@ ); productRefGroup = E72B2C07226939E3009A9438 /* Products */; projectDirPath = ""; + projectReferences = ( + { + ProductGroup = F53ED966292D7000002C3413 /* Products */; + ProjectRef = F53ED965292D7000002C3413 /* ResearchKit.xcodeproj */; + }, + ); projectRoot = ""; targets = ( E72B2C05226939E3009A9438 /* OCKSample */, @@ -645,6 +831,23 @@ }; /* End PBXProject section */ +/* Begin PBXReferenceProxy section */ + F53ED96C292D7000002C3413 /* ResearchKit.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = ResearchKit.framework; + remoteRef = F53ED96B292D7000002C3413 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + F53ED96E292D7000002C3413 /* ResearchKitTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = ResearchKitTests.xctest; + remoteRef = F53ED96D292D7000002C3413 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + /* Begin PBXResourcesBuildPhase section */ 5173CB8523C3A846007655A0 /* Resources */ = { isa = PBXResourcesBuildPhase; @@ -717,37 +920,54 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F545001F29400F96009A994F /* OCKEventAggregator.swift in Sources */, 70F921B327CAC16E00368CEC /* LocalSyncSessionDelegate.swift in Sources */, + F53ED98E292D8120002C3413 /* CheckIn.swift in Sources */, + F5ADA23E28F9FA4600D5317F /* AppearanceStyle.swift in Sources */, 70221F3928D8C10800971195 /* StoreManagerKey.swift in Sources */, + F53ED97C292D7C66002C3413 /* OCKAnyEvent.swift in Sources */, 70A98D63278A268B009B58F2 /* ColorStyler.swift in Sources */, 918FDEB8271B49060045A0EF /* Logger.swift in Sources */, 91AD923B24A4C42D00925D4D /* CareView.swift in Sources */, 70F03AA427860B2500E5AFB4 /* OCKOutcome.swift in Sources */, + F53ED98F292D8123002C3413 /* Onboard.swift in Sources */, 70F921A927CA9A4000368CEC /* CustomStylerKey.swift in Sources */, + F53ED987292D7E86002C3413 /* Survey.swift in Sources */, 7075151E28DE1A8300A57A0C /* MainView.swift in Sources */, 70A98D62278A2683009B58F2 /* Styler.swift in Sources */, 91693818271B5E1600A634ED /* Constants.swift in Sources */, 7075152028DE1BB400A57A0C /* LoginView.swift in Sources */, 70F921B227CAC16E00368CEC /* RemoteSessionDelegate.swift in Sources */, + F545000E294009F4009A994F /* OCKTaskController.swift in Sources */, 70221F2F28D7CDE400971195 /* AppDelegate+ParseRemoteDelegate.swift in Sources */, + F5450011294009FB009A994F /* OCKTaskEvents.swift in Sources */, 7083A857279CA40F00B3832E /* PCKUtility.swift in Sources */, 91AD923924A4C42D00925D4D /* OCKWatchSampleApp.swift in Sources */, 70F03AA627860B2500E5AFB4 /* OCKPatient+Parse.swift in Sources */, + F5144B00292C749700DA41A6 /* OCKSynchronizedStoreManager.swift in Sources */, 70221F3828D8C0CB00971195 /* AppDelegateKey.swift in Sources */, 9103AA5227B8C913002C921E /* FontColorKey.swift in Sources */, + F53ED985292D7E86002C3413 /* Surveyable.swift in Sources */, + F5ADA24028F9FA4B00D5317F /* AnimationStyle.swift in Sources */, 707CC719254DA91900116728 /* OCKLocalization.swift in Sources */, 7007759B252229C900EC0EDA /* User.swift in Sources */, 70F03A9B27860A2000E5AFB4 /* OCKAnyEvent+CustomStringConvertable.swift in Sources */, 70F03A912786073300E5AFB4 /* CareViewModel.swift in Sources */, 70A98D68278A2DF1009B58F2 /* TintColorKey.swift in Sources */, 918FDEB9271B493A0045A0EF /* Installation.swift in Sources */, + F545001429400A87009A994F /* CardViewModel.swift in Sources */, + F545001C29400B02009A994F /* ScheduleUtility.swift in Sources */, 70F03A9D27860A5600E5AFB4 /* OCKSynchronizedStoreManager+Publishers.swift in Sources */, + F5ADA23F28F9FA4900D5317F /* DimensionStyle.swift in Sources */, + F53ED990292D8124002C3413 /* RangeOfMotion.swift in Sources */, 70F03AA527860B2500E5AFB4 /* OCKOutcomeValue+Identifiable.swift in Sources */, 91AD923F24A4C42D00925D4D /* NotificationController.swift in Sources */, + F545000B294009DB009A994F /* OCKAnyOutcome.swift in Sources */, 70CF66E528E1E74C00FAE977 /* TintColorFlipKey.swift in Sources */, 70F03A9C27860A2000E5AFB4 /* OCKStore.swift in Sources */, 70C0D474279BA492003DA141 /* Utility.swift in Sources */, 91AD923D24A4C42D00925D4D /* AppDelegate.swift in Sources */, + F53ED971292D759F002C3413 /* OCKTask+Custom.swift in Sources */, 91AD924124A4C42D00925D4D /* NotificationView.swift in Sources */, 70308886258273D400FFABB6 /* LoginViewModel.swift in Sources */, 70F921B127CAC16E00368CEC /* SessionDelegate.swift in Sources */, @@ -758,45 +978,79 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F53ED984292D7E86002C3413 /* Surveyable.swift in Sources */, E7440E4F229477F7007AD30A /* CareViewController.swift in Sources */, 918FDEB5271B40590045A0EF /* Installation.swift in Sources */, + F53ED977292D76A0002C3413 /* Consent.swift in Sources */, + F545001329400A87009A994F /* CardViewModel.swift in Sources */, + F545000D294009F4009A994F /* OCKTaskController.swift in Sources */, 70F921AC27CABE3000368CEC /* SessionDelegate.swift in Sources */, 70F0EFEA28C2EE6C0005B5A2 /* AppDelegateKey.swift in Sources */, + F53ED97B292D7C66002C3413 /* OCKAnyEvent.swift in Sources */, 7036E64025717F85006E9A3C /* Constants.swift in Sources */, 918FDEBC271B4E4A0045A0EF /* Calendar+Dates.swift in Sources */, 70F03AA127860AB700E5AFB4 /* OCKOutcome.swift in Sources */, + F5144AFF292C71FD00DA41A6 /* OCKSynchronizedStoreManager.swift in Sources */, 7036E4CE256E9A0C006E9A3C /* ContactView.swift in Sources */, 70F03A972786098F00E5AFB4 /* OCKStore.swift in Sources */, + F5144AFB292C4B2300DA41A6 /* MyContactViewController.swift in Sources */, 70DFD80B2567074500B9DB12 /* LoginView.swift in Sources */, 70F03AA827860E7700E5AFB4 /* FontColorKey.swift in Sources */, 9169381B271B64E100A634ED /* Styler.swift in Sources */, 70F921A827CA9A3A00368CEC /* CustomStylerKey.swift in Sources */, 70221F3428D8ABBE00971195 /* AppDelegate+UIApplicationDelegate.swift in Sources */, + F545001E29400F96009A994F /* OCKEventAggregator.swift in Sources */, + F545001B29400B02009A994F /* ScheduleUtility.swift in Sources */, 9169381D271B650700A634ED /* ColorStyler.swift in Sources */, + F5450010294009FB009A994F /* OCKTaskEvents.swift in Sources */, E72B2C0A226939E3009A9438 /* AppDelegate.swift in Sources */, + F52CF9AC28F9E4BF004EEBF0 /* AppearanceStyle.swift in Sources */, + F583B698290B56BD00FE1C5A /* OCKTask+Custom.swift in Sources */, + F5144AF3292C489800DA41A6 /* ProfileImageView.swift in Sources */, 7036E4C4256E0A48006E9A3C /* CareView.swift in Sources */, 70F921B027CABED600368CEC /* LocalSyncSessionDelegate.swift in Sources */, + F5144AED292C452200DA41A6 /* CustomContactViewController.swift in Sources */, + F5BE45A82936AD0B008D3CCE /* CustomFeaturedContentView.swift in Sources */, + F583B69C290B619500FE1C5A /* HealthKitTaskViewModel.swift in Sources */, 70F03A932786087800E5AFB4 /* OCKAnyEvent+CustomStringConvertable.swift in Sources */, 70221F2828D7809800971195 /* MainTabView.swift in Sources */, 918FDEC3271B4E950045A0EF /* TintColorKey.swift in Sources */, + F545001829400AD4009A994F /* CustomCardView.swift in Sources */, 70CF66E428E1E74C00FAE977 /* TintColorFlipKey.swift in Sources */, + F545001929400AD4009A994F /* CustomCardViewModel.swift in Sources */, + F583B69A290B612400FE1C5A /* HealthKitTaskView.swift in Sources */, + F5144AF1292C488A00DA41A6 /* ImagePicker.swift in Sources */, 70F03AA327860AFF00E5AFB4 /* OCKPatient+Parse.swift in Sources */, + F54500242940100E009A994F /* InsightsViewController.swift in Sources */, 70F0EFE828C2EC050005B5A2 /* OCKSampleApp.swift in Sources */, + F5144AF5292C48F300DA41A6 /* OCKBiologicalSex+Hashable.swift in Sources */, 918FDEB7271B41FF0045A0EF /* Logger.swift in Sources */, 70F03A9F27860A8800E5AFB4 /* OCKOutcomeValue+Identifiable.swift in Sources */, 70221F2A28D7BE0600971195 /* AppDelegate+ParseRemoteDelegate.swift in Sources */, + F53ED98D292D7E8F002C3413 /* CheckIn.swift in Sources */, 7036E4D3256EBE35006E9A3C /* MainView.swift in Sources */, 918FDEC5271B4EA70045A0EF /* StoreManagerKey.swift in Sources */, 91693822271B897200A634ED /* Utility.swift in Sources */, + F5144AF9292C4B2000DA41A6 /* MyContactView.swift in Sources */, + F53ED98C292D7E8F002C3413 /* RangeOfMotion.swift in Sources */, + F53ED980292D7D9B002C3413 /* SurveyViewSynchronizer.swift in Sources */, + F583B691290B1C5100FE1C5A /* TaskView.swift in Sources */, + F52CF9AE28F9E4E9004EEBF0 /* DimensionStyle.swift in Sources */, 707CC718254DA91900116728 /* OCKLocalization.swift in Sources */, + F53ED98B292D7E8F002C3413 /* Onboard.swift in Sources */, E7C37849228F887800E982D8 /* TipView.swift in Sources */, 70077597252228E900EC0EDA /* User.swift in Sources */, 70F03A9927860A0800E5AFB4 /* OCKSynchronizedStoreManager+Publishers.swift in Sources */, + F545000A294009DB009A994F /* OCKAnyOutcome.swift in Sources */, + F52CF9AA28F9E398004EEBF0 /* AnimationStyle.swift in Sources */, 7036E4C9256E1A6F006E9A3C /* ProfileView.swift in Sources */, 7083A856279CA40A00B3832E /* PCKUtility.swift in Sources */, 70F921AE27CABE7700368CEC /* RemoteSessionDelegate.swift in Sources */, 70F03A952786093B00E5AFB4 /* OCKHealthKitPassthroughStore.swift in Sources */, 7036E4BF256DA089006E9A3C /* LoginViewModel.swift in Sources */, + F53ED986292D7E86002C3413 /* Survey.swift in Sources */, + F545002229400FFD009A994F /* InsightsView.swift in Sources */, + F583B693290B1C8F00FE1C5A /* TaskViewModel.swift in Sources */, 7036E517256F2413006E9A3C /* ProfileViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/OCKSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/OCKSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata old mode 100644 new mode 100755 diff --git a/OCKSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/OCKSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist old mode 100644 new mode 100755 diff --git a/OCKSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/OCKSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved old mode 100644 new mode 100755 diff --git a/OCKSample.xcodeproj/xcshareddata/xcschemes/OCKSample.xcscheme b/OCKSample.xcodeproj/xcshareddata/xcschemes/OCKSample.xcscheme old mode 100644 new mode 100755 diff --git a/OCKSample.xctestplan b/OCKSample.xctestplan old mode 100644 new mode 100755 diff --git a/OCKSample/AppDelegate.swift b/OCKSample/AppDelegate.swift old mode 100644 new mode 100755 diff --git a/OCKSample/Constants.swift b/OCKSample/Constants.swift old mode 100644 new mode 100755 index 555d895..f40a52d --- a/OCKSample/Constants.swift +++ b/OCKSample/Constants.swift @@ -64,7 +64,7 @@ extension AppError: LocalizedError { } enum Constants { - static let parseConfigFileName = "ParseCareKit" + static let parseConfigFileName = "ParseCareKit" // -heroku static let iOSParseCareStoreName = "iOSParseStore" static let iOSLocalCareStoreName = "iOSLocalStore" static let watchOSParseCareStoreName = "watchOSParseStore" @@ -73,23 +73,53 @@ enum Constants { static let parseUserSessionTokenKey = "requestParseSessionToken" static let requestSync = "requestSync" static let progressUpdate = "progressUpdate" - static let finishedAskingForPermission = "finishedAskingForPermission" static let completedFirstSyncAfterLogin = "completedFirstSyncAfterLogin" + static let finishedAskingForPermission = "finishedAskingForPermission" + static let shouldRefreshView = "shouldRefreshView" static let userLoggedIn = "userLoggedIn" static let storeInitialized = "storeInitialized" static let userTypeKey = "userType" + static let card = "card" + static let survey = "survey" } enum MainViewPath { case tabs } +enum CareKitCard: String, CaseIterable, Identifiable { + var id: Self { self } + case button = "Button" + case checklist = "Checklist" + case featured = "Featured" + case grid = "Grid" + case instruction = "Instruction" + case labeledValue = "Labeled Value" + case link = "Link" + case numericProgress = "Numeric Progress" + case simple = "Simple" + case survey = "Survey" + case custom = "Custom" // xTODO: Should add any custom card you make to this enum. +} + +enum CarePlanID: String, CaseIterable, Identifiable { + var id: Self { self } + case health // Add custom id's for your Care Plans, these are examples + case checkIn +} + enum TaskID { static let doxylamine = "doxylamine" static let nausea = "nausea" static let stretch = "stretch" static let kegels = "kegels" static let steps = "steps" + static let repetition = "repetition" + static let washFace = "washFace" + static let dirtiness = "dirtiness" + static let shower = "shower" + static let shampoo = "shampoo" + static let towels = "towels" static var ordered: [String] { [Self.steps, Self.doxylamine, Self.kegels, Self.stretch, Self.nausea] diff --git a/OCKSample/Environment/AppDelegateKey.swift b/OCKSample/Environment/AppDelegateKey.swift old mode 100644 new mode 100755 diff --git a/OCKSample/Environment/CustomStylerKey.swift b/OCKSample/Environment/CustomStylerKey.swift old mode 100644 new mode 100755 diff --git a/OCKSample/Environment/FontColorKey.swift b/OCKSample/Environment/FontColorKey.swift old mode 100644 new mode 100755 diff --git a/OCKSample/Environment/StoreManagerKey.swift b/OCKSample/Environment/StoreManagerKey.swift old mode 100644 new mode 100755 diff --git a/OCKSample/Environment/TintColorFlipKey.swift b/OCKSample/Environment/TintColorFlipKey.swift old mode 100644 new mode 100755 index 37d924a..0157c67 --- a/OCKSample/Environment/TintColorFlipKey.swift +++ b/OCKSample/Environment/TintColorFlipKey.swift @@ -12,7 +12,7 @@ import SwiftUI struct TintColorFlipKey: EnvironmentKey { static var defaultValue: UIColor { #if os(iOS) - return UIColor { $0.userInterfaceStyle == .light ? #colorLiteral(red: 0.06253327429, green: 0.6597633362, blue: 0.8644603491, alpha: 1) : #colorLiteral(red: 0, green: 0.2858072221, blue: 0.6897063851, alpha: 1) } + return UIColor { $0.userInterfaceStyle == .light ? #colorLiteral(red: 1, green: 0.622412324, blue: 0.7744814754, alpha: 1) : #colorLiteral(red: 0.721568644, green: 0.8862745166, blue: 0.5921568871, alpha: 1) } #else return #colorLiteral(red: 0.06253327429, green: 0.6597633362, blue: 0.8644603491, alpha: 1) #endif diff --git a/OCKSample/Environment/TintColorKey.swift b/OCKSample/Environment/TintColorKey.swift old mode 100644 new mode 100755 index 4205df2..8390ca9 --- a/OCKSample/Environment/TintColorKey.swift +++ b/OCKSample/Environment/TintColorKey.swift @@ -12,7 +12,7 @@ import SwiftUI struct TintColorKey: EnvironmentKey { static var defaultValue: UIColor { #if os(iOS) - return UIColor { $0.userInterfaceStyle == .light ? #colorLiteral(red: 0, green: 0.2858072221, blue: 0.6897063851, alpha: 1) : #colorLiteral(red: 0.06253327429, green: 0.6597633362, blue: 0.8644603491, alpha: 1) } + return UIColor { $0.userInterfaceStyle == .light ? #colorLiteral(red: 0.721568644, green: 0.8862745166, blue: 0.5921568871, alpha: 1) : #colorLiteral(red: 1, green: 0.6348608732, blue: 0.8093153834, alpha: 1) } #else return #colorLiteral(red: 0, green: 0.2855202556, blue: 0.6887390018, alpha: 1) #endif diff --git a/OCKSample/Extensions/AppDelegate+ParseRemoteDelegate.swift b/OCKSample/Extensions/AppDelegate+ParseRemoteDelegate.swift old mode 100644 new mode 100755 diff --git a/OCKSample/Extensions/AppDelegate+UIApplicationDelegate.swift b/OCKSample/Extensions/AppDelegate+UIApplicationDelegate.swift old mode 100644 new mode 100755 diff --git a/OCKSample/Extensions/Calendar+Dates.swift b/OCKSample/Extensions/Calendar+Dates.swift old mode 100644 new mode 100755 diff --git a/OCKSample/Extensions/Logger.swift b/OCKSample/Extensions/Logger.swift old mode 100644 new mode 100755 index ca0ee32..ef01b28 --- a/OCKSample/Extensions/Logger.swift +++ b/OCKSample/Extensions/Logger.swift @@ -17,6 +17,7 @@ extension Logger { static let localSessionDelegate = Logger(subsystem: subsystem, category: "LocalSessionDelegate") static let utility = Logger(subsystem: subsystem, category: "Utility") static let contact = Logger(subsystem: subsystem, category: "Contact") + static let myContact = Logger(subsystem: subsystem, category: "MyContact") static let login = Logger(subsystem: subsystem, category: "Login") static let feed = Logger(subsystem: subsystem, category: "Feed") static let watch = Logger(subsystem: subsystem, category: "Watch") diff --git a/OCKSample/Extensions/OCKAnyEvent+CustomStringConvertable.swift b/OCKSample/Extensions/OCKAnyEvent+CustomStringConvertable.swift old mode 100644 new mode 100755 diff --git a/OCKSample/Extensions/OCKAnyEvent.swift b/OCKSample/Extensions/OCKAnyEvent.swift new file mode 100755 index 0000000..e84e840 --- /dev/null +++ b/OCKSample/Extensions/OCKAnyEvent.swift @@ -0,0 +1,17 @@ +// +// OCKAnyEvent.swift +// OCKSample +// +// Created by Corey Baker on 11/11/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// +import CareKitStore + +extension OCKAnyEvent { + + func answer(kind: String) -> Double { + let values = outcome?.values ?? [] + let match = values.first(where: { $0.kind == kind }) + return match?.doubleValue ?? 0 + } +} diff --git a/OCKSample/Extensions/OCKAnyOutcome.swift b/OCKSample/Extensions/OCKAnyOutcome.swift new file mode 100644 index 0000000..9d25af1 --- /dev/null +++ b/OCKSample/Extensions/OCKAnyOutcome.swift @@ -0,0 +1,68 @@ +// +// OCKAnyOutcome.swift +// OCKSample +// +// Created by Corey Baker on 12/3/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import CareKitStore + +extension OCKAnyOutcome { + + func answerDouble(kind: String) -> [Double] { + let doubleValues = values.compactMap({ value -> Double? in + guard value.kind == kind else { + return nil + } + guard let intValue = value.integerValue else { + return value.doubleValue + } + return Double(intValue) + }) + return doubleValues + } + + func answerString(kind: String) -> [String] { + let stringValues = values.compactMap { value -> String? in + guard value.kind == kind else { + return nil + } + return value.stringValue + } + return stringValues + } + + func sortedOutcomeValuesByRecency() -> Self { + guard !self.values.isEmpty else { return self } + var newOutcome = self + let sortedValues = newOutcome.values.sorted { + $0.createdDate > $1.createdDate + } + + newOutcome.values = sortedValues + return newOutcome + } + + func sortedOutcomeValues() -> Self { + guard !self.values.isEmpty else { return self } + var newOutcome = self + let sortedValues = newOutcome.values.sorted { + if let value0 = $0.dateValue, + let value1 = $1.dateValue { + return value0 > value1 + } else if let value0 = $0.integerValue, + let value1 = $1.integerValue { + return value0 > value1 + } else if let value0 = $0.doubleValue, + let value1 = $1.doubleValue { + return value0 > value1 + } else { + return false + } + } + + newOutcome.values = sortedValues + return newOutcome + } +} diff --git a/OCKSample/Extensions/OCKBiologicalSex+Hashable.swift b/OCKSample/Extensions/OCKBiologicalSex+Hashable.swift new file mode 100755 index 0000000..ac1644c --- /dev/null +++ b/OCKSample/Extensions/OCKBiologicalSex+Hashable.swift @@ -0,0 +1,13 @@ +// +// OCKBiologicalSex+Hashable.swift +// OCKSample +// +// Created by Corey Baker on 11/7/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import CareKitStore + + // Needed to use OCKBiologicalSex in a Picker. + // Simple conformance to hashable protocol. + extension OCKBiologicalSex: Hashable { } diff --git a/OCKSample/Extensions/OCKEventAggregator.swift b/OCKSample/Extensions/OCKEventAggregator.swift new file mode 100644 index 0000000..c191b6f --- /dev/null +++ b/OCKSample/Extensions/OCKEventAggregator.swift @@ -0,0 +1,109 @@ +// +// OCKEventAggregator.swift +// OCKSample +// +// Created by Corey Baker on 12/3/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// +import CareKitStore +import Foundation + +extension OCKEventAggregator { + + static func aggregatorMean(_ kind: String? = nil) -> OCKEventAggregator { + guard let kind = kind else { + return OCKEventAggregator.custom { events -> Double in + + let totalCompleted = Double(events.map { $0.outcome?.values.count ?? 0 }.reduce(0, +)) + let tempSumOfCompleted = events.compactMap { + $0.outcome?.values.compactMap { value -> Double in + guard let intValue = value.integerValue else { + guard let boolValue = value.booleanValue else { + return value.doubleValue ?? 0.0 + } + return boolValue ? 1.0 : 0.0 + } + return Double(intValue) + }.reduce(0, +) + } + let sumOfCompleted = Double(tempSumOfCompleted.compactMap { $0 }.reduce(0, +)) + + if totalCompleted == 0 { + return 0.0 + } else { + return sumOfCompleted / totalCompleted + } + } + } + + return OCKEventAggregator.custom { events -> Double in + + let completed = events.compactMap { event -> [Double]? in + event.outcome?.answerDouble(kind: kind) + }.flatMap { $0 } + + let totalCompleted = Double(completed.count) + let sumOfCompleted = Double(completed.compactMap { $0 }.reduce(0, +)) + + if totalCompleted == 0 { + return 0.0 + } else { + return sumOfCompleted / totalCompleted + } + } + } + + static func aggregatorMedian(_ kind: String? = nil) -> OCKEventAggregator { + guard let kind = kind else { + return OCKEventAggregator.custom { events -> Double in + + let tempAllValues = events.compactMap { + $0.outcome?.values.compactMap { value -> Double in + guard let intValue = value.integerValue else { + guard let boolValue = value.booleanValue else { + return value.doubleValue ?? 0.0 + } + return boolValue ? 1.0 : 0.0 + } + return Double(intValue) + } + } + var sortedAllValues = tempAllValues.flatMap { $0 } + sortedAllValues.sort() + + if sortedAllValues.isEmpty { + return 0.0 + } else if (sortedAllValues.count % 2) == 0 { + let index = sortedAllValues.count / 2 + return (sortedAllValues[index] + sortedAllValues[index - 1]) / 2.0 + } else { + return sortedAllValues[sortedAllValues.count / 2] + } + } + } + + return OCKEventAggregator.custom { events -> Double in + + var completed = events.compactMap { event -> [Double]? in + event.outcome?.answerDouble(kind: kind) + }.flatMap { $0 } + + completed.sort() + + if completed.isEmpty { + return 0.0 + } else if (completed.count % 2) == 0 { + let index = completed.count / 2 + return (completed[index] + completed[index - 1]) / 2.0 + } else { + return completed[completed.count / 2] + } + } + } + + static func aggregatorStreak(_ kind: String? = nil) -> OCKEventAggregator { + return OCKEventAggregator.custom { events -> Double in + Double(events.map { $0.outcome?.values.first != nil ? 1 : 0 }.reduce(0, +)) + } + } +} diff --git a/OCKSample/Extensions/OCKHealthKitPassthroughStore.swift b/OCKSample/Extensions/OCKHealthKitPassthroughStore.swift old mode 100644 new mode 100755 index 9ae9a6f..eb9fa48 --- a/OCKSample/Extensions/OCKHealthKitPassthroughStore.swift +++ b/OCKSample/Extensions/OCKHealthKitPassthroughStore.swift @@ -42,6 +42,9 @@ extension OCKHealthKitPassthroughStore { } } + /* + xTODO: You need to tie an OCPatient and CarePlan to these tasks, + */ func populateSampleData() async throws { let schedule = OCKSchedule.dailyAtTime( diff --git a/OCKSample/Extensions/OCKOutcome.swift b/OCKSample/Extensions/OCKOutcome.swift old mode 100644 new mode 100755 diff --git a/OCKSample/Extensions/OCKOutcomeValue+Identifiable.swift b/OCKSample/Extensions/OCKOutcomeValue+Identifiable.swift old mode 100644 new mode 100755 diff --git a/OCKSample/Extensions/OCKPatient+Parse.swift b/OCKSample/Extensions/OCKPatient+Parse.swift old mode 100644 new mode 100755 diff --git a/OCKSample/Extensions/OCKStore.swift b/OCKSample/Extensions/OCKStore.swift old mode 100644 new mode 100755 index 07847cb..6f137a6 --- a/OCKSample/Extensions/OCKStore.swift +++ b/OCKSample/Extensions/OCKStore.swift @@ -43,6 +43,38 @@ extension OCKStore { } } + func populateCarePlans(patientUUID: UUID? = nil) async throws { + let checkInCarePlan = OCKCarePlan(id: CarePlanID.checkIn.rawValue, + title: "Check in Care Plan", + patientUUID: patientUUID) + try await AppDelegateKey + .defaultValue? + .storeManager + .addCarePlansIfNotPresent([checkInCarePlan], + patientUUID: patientUUID) + } + + @MainActor + class func getCarePlanUUIDs() async throws -> [CarePlanID: UUID] { + var results = [CarePlanID: UUID]() + + guard let store = AppDelegateKey.defaultValue?.store else { + return results + } + + var query = OCKCarePlanQuery(for: Date()) + query.ids = [CarePlanID.health.rawValue, + CarePlanID.checkIn.rawValue] + + let foundCarePlans = try await store.fetchCarePlans(query: query) + // Populate the dictionary for all CarePlan's + CarePlanID.allCases.forEach { carePlanID in + results[carePlanID] = foundCarePlans + .first(where: { $0.id == carePlanID.rawValue })?.uuid + } + return results + } + func addContactsIfNotPresent(_ contacts: [OCKContact]) async throws { let contactIdsToAdd = contacts.compactMap { $0.id } @@ -72,12 +104,17 @@ extension OCKStore { } // Adds tasks and contacts into the store - func populateSampleData() async throws { + func populateSampleData(_ patientUUID: UUID? = nil) async throws { + try await populateCarePlans(patientUUID: patientUUID) + let carePlanUUIDs = try await Self.getCarePlanUUIDs() let thisMorning = Calendar.current.startOfDay(for: Date()) - let aFewDaysAgo = Calendar.current.date(byAdding: .day, value: -4, to: thisMorning)! - let beforeBreakfast = Calendar.current.date(byAdding: .hour, value: 8, to: aFewDaysAgo)! - let afterLunch = Calendar.current.date(byAdding: .hour, value: 14, to: aFewDaysAgo)! + guard let aFewDaysAgo = Calendar.current.date(byAdding: .day, value: -4, to: thisMorning), + let beforeBreakfast = Calendar.current.date(byAdding: .hour, value: 8, to: aFewDaysAgo), + let afterLunch = Calendar.current.date(byAdding: .hour, value: 14, to: aFewDaysAgo) else { + Logger.ockStore.error("Could not unwrap calendar. Should never hit") + return + } let schedule = OCKSchedule(composing: [ OCKScheduleElement(start: beforeBreakfast, end: nil, @@ -87,10 +124,11 @@ extension OCKStore { interval: DateComponents(day: 2)) ]) - var doxylamine = OCKTask(id: TaskID.doxylamine, title: "Take Doxylamine", + var washFace = OCKTask(id: TaskID.washFace, title: "Wash Your Face", carePlanUUID: nil, schedule: schedule) - doxylamine.instructions = "Take 25mg of doxylamine when you experience nausea." - doxylamine.asset = "pills.fill" + washFace.instructions = "Make sure to wash your face at least twice a day." + washFace.asset = "pills.fill" + washFace.card = .checklist let nauseaSchedule = OCKSchedule(composing: [ OCKScheduleElement(start: beforeBreakfast, end: nil, interval: DateComponents(day: 1), @@ -100,22 +138,44 @@ extension OCKStore { var nausea = OCKTask(id: TaskID.nausea, title: "Track your nausea", carePlanUUID: nil, schedule: nauseaSchedule) nausea.impactsAdherence = false - nausea.instructions = "Tap the button below anytime you experience nausea." + nausea.instructions = "Tap the button below anytime you feel dirty." nausea.asset = "bed.double" + nausea.card = .button + + var gallonsUsed = OCKTask(id: TaskID.repetition, + title: "Track your water usage", + carePlanUUID: nil, + schedule: nauseaSchedule) + gallonsUsed.impactsAdherence = false + gallonsUsed.instructions = "Input how much water you used in gallons." + gallonsUsed.asset = "repeat.circle" + gallonsUsed.card = .custom let kegelElement = OCKScheduleElement(start: beforeBreakfast, end: nil, interval: DateComponents(day: 2)) let kegelSchedule = OCKSchedule(composing: [kegelElement]) - var kegels = OCKTask(id: TaskID.kegels, title: "Kegel Exercises", carePlanUUID: nil, schedule: kegelSchedule) - kegels.impactsAdherence = true - kegels.instructions = "Perform kegel exercies" + // swiftlint:disable:next line_length + var shampoo = OCKTask(id: TaskID.shampoo, title: "Do you have shampoo and soap?", carePlanUUID: nil, schedule: kegelSchedule) + shampoo.impactsAdherence = true + shampoo.instructions = "Ensure shampoo and soap is in stock" + shampoo.card = .simple let stretchElement = OCKScheduleElement(start: beforeBreakfast, end: nil, interval: DateComponents(day: 1)) let stretchSchedule = OCKSchedule(composing: [stretchElement]) - var stretch = OCKTask(id: "stretch", title: "Stretch", carePlanUUID: nil, schedule: stretchSchedule) - stretch.impactsAdherence = true - stretch.asset = "figure.walk" + // swiftlint:disable:next line_length + var shower = OCKTask(id: "shower", title: "Did you take a shower today?", carePlanUUID: nil, schedule: stretchSchedule) + shower.impactsAdherence = true + shower.asset = "figure.walk" + shower.card = .instruction - try await addTasksIfNotPresent([nausea, doxylamine, kegels, stretch]) + // swiftlint:disable:next line_length + var towels = OCKTask(id: TaskID.towels, title: "Do you have clean towels?", carePlanUUID: nil, schedule: nauseaSchedule) + towels.impactsAdherence = false + towels.instructions = "Ensure shampoo and soap is in stock" + towels.card = .grid + + try await addTasksIfNotPresent([nausea, washFace, shampoo, shower, towels, gallonsUsed]) + try await addOnboardTask(carePlanUUIDs[.health]) + try await addSurveyTasks(carePlanUUIDs[.checkIn]) var contact1 = OCKContact(id: "jane", givenName: "Jane", familyName: "Daniels", carePlanUUID: nil) @@ -153,4 +213,90 @@ extension OCKStore { try await addContactsIfNotPresent([contact1, contact2]) } + + func addOnboardTask(_ carePlanUUID: UUID? = nil) async throws { + let onboardSchedule = OCKSchedule.dailyAtTime( + hour: 0, minutes: 0, + start: Date(), end: nil, + text: "Task Due!", + duration: .allDay + ) + + var onboardTask = OCKTask( + id: Onboard.identifier(), + title: "Onboard", + carePlanUUID: carePlanUUID, + schedule: onboardSchedule + ) + onboardTask.instructions = "You'll need to agree to some terms and conditions before we get started!" + onboardTask.impactsAdherence = false + onboardTask.card = .survey + onboardTask.survey = .onboard + + try await addTasksIfNotPresent([onboardTask]) + } + + func addSurveyTasks(_ carePlanUUID: UUID? = nil) async throws { + let checkInSchedule = OCKSchedule.dailyAtTime( + hour: 8, minutes: 0, + start: Date(), end: nil, + text: nil + ) + + var checkInTask = OCKTask( + id: CheckIn.identifier(), + title: "Shower Check In", + carePlanUUID: carePlanUUID, + schedule: checkInSchedule + ) + checkInTask.card = .survey + checkInTask.survey = .checkIn + + let thisMorning = Calendar.current.startOfDay(for: Date()) + + let nextWeek = Calendar.current.date( + byAdding: .weekOfYear, + value: 1, + to: Date() + )! + + let nextMonth = Calendar.current.date( + byAdding: .month, + value: 1, + to: thisMorning + ) + + let dailyElement = OCKScheduleElement( + start: thisMorning, + end: nextWeek, + interval: DateComponents(day: 1), + text: nil, + targetValues: [], + duration: .allDay + ) + + let weeklyElement = OCKScheduleElement( + start: nextWeek, + end: nextMonth, + interval: DateComponents(weekOfYear: 1), + text: nil, + targetValues: [], + duration: .allDay + ) + + let rangeOfMotionCheckSchedule = OCKSchedule( + composing: [dailyElement, weeklyElement] + ) + + var rangeOfMotionTask = OCKTask( + id: RangeOfMotion.identifier(), + title: "Shoulder Mobility", + carePlanUUID: carePlanUUID, + schedule: rangeOfMotionCheckSchedule + ) + rangeOfMotionTask.card = .survey + rangeOfMotionTask.survey = .rangeOfMotion + + try await addTasksIfNotPresent([checkInTask, rangeOfMotionTask]) + } } diff --git a/OCKSample/Extensions/OCKSynchronizedStoreManager+Publishers.swift b/OCKSample/Extensions/OCKSynchronizedStoreManager+Publishers.swift old mode 100644 new mode 100755 index 217eaf9..dcf6b2a --- a/OCKSample/Extensions/OCKSynchronizedStoreManager+Publishers.swift +++ b/OCKSample/Extensions/OCKSynchronizedStoreManager+Publishers.swift @@ -47,6 +47,42 @@ extension OCKSynchronizedStoreManager { .prepend(presentValuePublisher)) } + // MARK: Contacts + + func contactsPublisher(categories: [OCKStoreNotificationCategory]) -> AnyPublisher { + return AnyPublisher(notificationPublisher + .compactMap { $0 as? OCKContactNotification } + .filter { categories.contains($0.category) } + .map { $0.contact }) + } + + func publisher(forContactID id: String, + categories: [OCKStoreNotificationCategory]) -> AnyPublisher { + return notificationPublisher + .compactMap { $0 as? OCKContactNotification } + .filter { $0.contact.id == id && categories.contains($0.category) } + .map { $0.contact } + .eraseToAnyPublisher() + } + + func publisher(forContact contact: OCKAnyContact, + categories: [OCKStoreNotificationCategory], + fetchImmediately: Bool = true) -> AnyPublisher { + let presentValuePublisher = Future({ completion in + self.store.fetchAnyContact(withID: contact.id) { result in + completion(.success((try? result.get()) ?? contact)) + } + }) + + let changePublisher = notificationPublisher + .compactMap { $0 as? OCKContactNotification } + .filter { $0.contact.id == contact.id && categories.contains($0.category) } + .map { $0.contact } + + // swiftlint:disable:next line_length + return fetchImmediately ? AnyPublisher(changePublisher.prepend(presentValuePublisher)) : AnyPublisher(changePublisher) + } + // MARK: Tasks func publisherForTasks(categories: [OCKStoreNotificationCategory]) -> AnyPublisher { diff --git a/OCKSample/Extensions/OCKSynchronizedStoreManager.swift b/OCKSample/Extensions/OCKSynchronizedStoreManager.swift new file mode 100755 index 0000000..3e6ead4 --- /dev/null +++ b/OCKSample/Extensions/OCKSynchronizedStoreManager.swift @@ -0,0 +1,118 @@ +// +// OCKSynchronizedStoreManager.swift +// OCKSample +// +// Created by Corey Baker on 11/7/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation +import CareKit +import CareKitStore +import CareKitUI +import os.log + +extension OCKSynchronizedStoreManager { + + /** + Adds an `OCKAnyCarePlan`*asynchronously* to `OCKStore` if it has not been added already. + + - parameter carePlans: The array of `OCKAnyCarePlan`'s to be added to the `OCKStore`. + - parameter patientUUID: The uuid of the `OCKPatient` to tie to the `OCKCarePlan`. Defaults to nil. + - throws: An error if there was a problem adding the missing `OCKAnyCarePlan`'s. + - note: `OCKAnyCarePlan`'s that have an existing `id` will not be added and will not cause errors to be thrown. + */ + func addCarePlansIfNotPresent(_ carePlans: [OCKAnyCarePlan], patientUUID: UUID? = nil) async throws { + let carePlanIdsToAdd = carePlans.compactMap { $0.id } + + // Prepare query to see if Care Plan are already added + var query = OCKCarePlanQuery(for: Date()) + query.ids = carePlanIdsToAdd + let foundCarePlans = try await store.fetchAnyCarePlans(query: query) + var carePlanNotInStore = [OCKAnyCarePlan]() + // Check results to see if there's a missing Care Plan + carePlans.forEach { potentialCarePlan in + if foundCarePlans.first(where: { $0.id == potentialCarePlan.id }) == nil { + // Check if can be casted to OCKCarePlan to add patientUUID + guard var mutableCarePlan = potentialCarePlan as? OCKCarePlan else { + carePlanNotInStore.append(potentialCarePlan) + return + } + mutableCarePlan.patientUUID = patientUUID + carePlanNotInStore.append(mutableCarePlan) + } + } + + // Only add if there's a new Care Plan + if carePlanNotInStore.count > 0 { + do { + _ = try await store.addAnyCarePlans(carePlanNotInStore) + // Logger.ockSynchronizedStoreManager.info("Added Care Plans into OCKStore!") + } catch { + // Logger.ockSynchronizedStoreManager.error("Error adding Care Plans: \(error.localizedDescription)") + } + } + } + + func addTasksIfNotPresent(_ tasks: [OCKAnyTask]) async throws { + let taskIdsToAdd = tasks.compactMap { $0.id } + + // Prepare query to see if tasks are already added + var query = OCKTaskQuery(for: Date()) + query.ids = taskIdsToAdd + + let foundTasks = try await store.fetchAnyTasks(query: query) + var tasksNotInStore = [OCKAnyTask]() + + // Check results to see if there's a missing task + tasks.forEach { potentialTask in + if foundTasks.first(where: { $0.id == potentialTask.id }) == nil { + tasksNotInStore.append(potentialTask) + } + } + + // Only add if there's a new task + if tasksNotInStore.count > 0 { + do { + _ = try await store.addAnyTasks(tasksNotInStore) + // Logger.ockSynchronizedStoreManager.info("Added tasks into OCKSynchronizedStoreManager!") + } catch { + // Logger.ockSynchronizedStoreManager.error("Error adding tasks: \(error.localizedDescription)") + } + } + } + + func addContactsIfNotPresent(_ contacts: [OCKAnyContact], carePlanUUID: UUID? = nil) async throws { + let contactIdsToAdd = contacts.compactMap { $0.id } + + // Prepare query to see if contacts are already added + var query = OCKContactQuery(for: Date()) + query.ids = contactIdsToAdd + + let foundContacts = try await store.fetchAnyContacts(query: query) + var contactsNotInStore = [OCKAnyContact]() + + // Check results to see if there's a missing task + contacts.forEach { potential in + if foundContacts.first(where: { $0.id == potential.id }) == nil { + // Check if can be casted to OCKCarePlan to add patientUUID + guard var mutableContact = potential as? OCKContact else { + contactsNotInStore.append(potential) + return + } + mutableContact.carePlanUUID = carePlanUUID + contactsNotInStore.append(mutableContact) + } + } + + // Only add if there's a new task + if contactsNotInStore.count > 0 { + do { + _ = try await store.addAnyContacts(contactsNotInStore) + // Logger.ockSynchronizedStoreManager.info("Added contacts into OCKSynchronizedStoreManager!") + } catch { + // Logger.ockSynchronizedStoreManager.error("Error adding contacts: \(error.localizedDescription)") + } + } + } +} diff --git a/OCKSample/Extensions/OCKTask+Custom.swift b/OCKSample/Extensions/OCKTask+Custom.swift new file mode 100755 index 0000000..33ffbb0 --- /dev/null +++ b/OCKSample/Extensions/OCKTask+Custom.swift @@ -0,0 +1,84 @@ +// +// OCKTask+Custom.swift +// OCKSample +// +// Created by on 10/27/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation +import CareKitStore + +extension OCKTask { + var card: CareKitCard { + get { + guard let cardInfo = userInfo?[Constants.card], + let careKitCard = CareKitCard(rawValue: cardInfo) else { + return .grid // Default card if none was saved + } + return careKitCard // Saved card type + } + set { + if userInfo == nil { + // Initialize userInfo with empty dictionary + userInfo = .init() + } + // Set the new card type + userInfo?[Constants.card] = newValue.rawValue + } + } + var survey: Survey { + get { + guard let surveyInfo = userInfo?[Constants.survey], + let surveyType = Survey(rawValue: surveyInfo) else { + return .checkIn // Default survey if none was saved + } + return surveyType // Saved survey type + } + set { + if userInfo == nil { + // Initialize userInfo with empty dictionary + userInfo = .init() + } + // Set the new card type + userInfo?[Constants.survey] = newValue.rawValue + } + } +} + +extension OCKHealthKitTask { + var card: CareKitCard { + get { + guard let cardInfo = userInfo?[Constants.card], + let careKitCard = CareKitCard(rawValue: cardInfo) else { + return .grid // Default card if none was saved + } + return careKitCard // Saved card type + } + set { + if userInfo == nil { + // Initialize userInfo with empty dictionary + userInfo = .init() + } + // Set the new card type + userInfo?[Constants.card] = newValue.rawValue + } + } + var survey: Survey { + get { + guard let surveyInfo = userInfo?[Constants.survey], + let surveyType = Survey(rawValue: surveyInfo) else { + return .checkIn // Default survey if none was saved + } + return surveyType // Saved survey type + } + set { + if userInfo == nil { + // Initialize userInfo with empty dictionary + userInfo = .init() + } + // Set the new card type + userInfo?[Constants.survey] = newValue.rawValue + } + } +} diff --git a/OCKSample/Extensions/OCKTaskController.swift b/OCKSample/Extensions/OCKTaskController.swift new file mode 100644 index 0000000..c34cf7e --- /dev/null +++ b/OCKSample/Extensions/OCKTaskController.swift @@ -0,0 +1,93 @@ +// +// OCKTaskController.swift +// OCKSample +// +// Created by Corey Baker on 12/4/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import CareKit +import CareKitStore +import UIKit + +extension OCKTaskController { + + func validatedViewModel() throws -> OCKTaskEvents { + guard !taskEvents.isEmpty else { + throw AppError.errorString("Empty task events") + } + return taskEvents + } + + func validatedEvent(forIndexPath indexPath: IndexPath) throws -> OCKAnyEvent { + guard let event = eventFor(indexPath: indexPath) else { + throw AppError.errorString("Invalid index path: \(indexPath)") + } + return event + } + + /// Append an outcome value to an event's outcome. + /// - Parameters: + /// - value: The value for the outcome value that is being created. + /// - indexPath: Index path of the event to which the outcome will be added. + /// - Returns: The saved outcome value. + /// - Throws: An error if the outcome value can't be saved. + func appendOutcomeValue( + value: OCKOutcomeValueUnderlyingType, + at indexPath: IndexPath) async throws -> OCKAnyOutcome { + try await withCheckedThrowingContinuation { continuation in + self.appendOutcomeValue(value: value, + at: indexPath, + completion: continuation.resume) + } + } + + /// Append an outcome value to an event's outcome. + /// - Parameters: + /// - value: The outcome value. + /// - indexPath: Index path of the event to which the outcome will be added. + /// - Returns: The saved outcome value. + /// - Throws: An error if the outcome value can't be saved. + func appendOutcomeValue( + value: OCKOutcomeValue, + at indexPath: IndexPath) async throws -> OCKAnyOutcome { + + let event: OCKAnyEvent + _ = try validatedViewModel() + event = try validatedEvent(forIndexPath: indexPath) + + // Update the outcome with the new value + guard var outcome = event.outcome else { + let outcome = try makeOutcomeFor(event: event, withValues: [value]) + return try await storeManager.store.addAnyOutcome(outcome) + } + outcome.values.append(value) + return try await storeManager.store.updateAnyOutcome(outcome) + } + + /// Set the completion state for an event. + /// - Parameters: + /// - indexPath: Index path of the event. + /// - values: Array of OCKOutcomeValue + /// - Returns: The updated outcome. + /// - Throws: An error if the outcome value can't be set. + func setEvent(atIndexPath indexPath: IndexPath, + values: [OCKOutcomeValue]) async throws -> OCKAnyOutcome { + try await withCheckedThrowingContinuation { continuation in + self.setEvent(atIndexPath: indexPath, + values: values, + completion: continuation.resume) + } + } + + /// Save the outcome for a particular event. + /// - Parameters: + /// - indexPath: Index path of the event. + /// - values: Array of `OCKOutcomeValue` + /// - Returns: The saved outcome. + /// - Throws: An error if the outcome value can't be saved. + func saveOutcomesForEvent(atIndexPath indexPath: IndexPath, + values: [OCKOutcomeValue]) async throws -> OCKAnyOutcome { + try await setEvent(atIndexPath: indexPath, values: values) + } +} diff --git a/OCKSample/Extensions/OCKTaskEvents.swift b/OCKSample/Extensions/OCKTaskEvents.swift new file mode 100644 index 0000000..330a864 --- /dev/null +++ b/OCKSample/Extensions/OCKTaskEvents.swift @@ -0,0 +1,93 @@ +// +// OCKTaskEvents.swift +// OCKSample +// +// Created by Corey Baker on 12/3/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import CareKit +import CareKitUI +import CareKitStore +import Foundation + +/// Helper properties and methods for using OCKTaskEvents. +extension OCKTaskEvents { + + /// The event for this view model. + var firstEvent: OCKAnyEvent? { + first?.first + } + + /// The first event task. + /// - note: If you need `OCKTask` or `OCKHealthKitTask`, you need to cast + /// this to the respective type. + var firstEventTask: OCKAnyTask? { + firstEvent?.task + } + + /// The first event task title. + var firstEventTitle: String { + firstEventTask?.title ?? "" + } + + /// The first event task instructions. + var firstTaskInstructions: String? { + firstEventTask?.instructions + } + + /// The first event detail. + var firstEventDetail: String? { + ScheduleUtility.scheduleLabel(for: firstEvent) + } + + /// The first event outcome. + /// - note: If you need `OCKOutcome` or `OCKHealthKitOutcome`, you need to cast + /// this to the respective type. + var firstEventOutcome: OCKAnyOutcome? { + firstEvent?.outcome?.sortedOutcomeValuesByRecency() + } + + /// Returns **true** if the first event is complete. + var isFirstEventComplete: Bool { + firstEventOutcome != nil + } + + /// The first event outcome values. + var firstEventOutcomeValues: [OCKOutcomeValue]? { + firstEventOutcome?.values + } + + /// The first event first outcome value. + var firstEventOutcomeFirstValue: OCKOutcomeValue? { + firstEventOutcomeValues?.first + } + + /// The first event first outcome value as a **Int**. + /// - note: Returns **0** if the first outcome is **nil**. + var firstEventOutcomeValueInt: Int { + firstEventOutcomeFirstValue?.integerValue ?? 0 + } + + /// The first event first outcome value as a **Double**. + /// - note: Returns **0.0** if the first outcome is **nil**. + var firstEventOutcomeValueDouble: Double? { + firstEventOutcomeFirstValue?.doubleValue + } + + /// The first event first outcome value as a **String**. + /// - note: Returns an empty **String** if the first outcome is **nil**. + var firstEventOutcomeValueString: String? { + firstEventOutcomeFirstValue?.stringValue + } + + /// The first event first outcome value as a **Date**. + var firstEventOutcomeValueDate: Date? { + firstEventOutcomeFirstValue?.dateValue + } + + /// The first event first outcome value as **Data**. + var firstEventOutcomeValueData: Data? { + firstEventOutcomeFirstValue?.dataValue + } +} diff --git a/OCKSample/Extensions/PCKUtility.swift b/OCKSample/Extensions/PCKUtility.swift old mode 100644 new mode 100755 diff --git a/OCKSample/Main/Care/CareView.swift b/OCKSample/Main/Care/CareView.swift old mode 100644 new mode 100755 diff --git a/OCKSample/Main/Care/CareViewController.swift b/OCKSample/Main/Care/CareViewController.swift old mode 100644 new mode 100755 index 2ede25e..4466a94 --- a/OCKSample/Main/Care/CareViewController.swift +++ b/OCKSample/Main/Care/CareViewController.swift @@ -36,7 +36,9 @@ import CareKit import CareKitStore import CareKitUI import os.log +import ResearchKit +// swiftlint:disable type_body_length class CareViewController: OCKDailyPageViewController { private var isSyncing = false @@ -60,8 +62,7 @@ class CareViewController: OCKDailyPageViewController { object: nil) NotificationCenter.default.addObserver(self, selector: #selector(reloadView(_:)), - // swiftlint:disable:next line_length - name: Notification.Name(rawValue: Constants.completedFirstSyncAfterLogin), + name: Notification.Name(rawValue: Constants.shouldRefreshView), object: nil) } @@ -133,24 +134,45 @@ class CareViewController: OCKDailyPageViewController { */ override func dailyPageViewController(_ dailyPageViewController: OCKDailyPageViewController, prepare listViewController: OCKListViewController, for date: Date) { - let isCurrentDay = Calendar.current.isDate(date, inSameDayAs: Date()) + Task { + guard await checkIfOnboardingIsComplete() else { + let onboardSurvey = Onboard() + let onboardCard = OCKSurveyTaskViewController(taskID: onboardSurvey.identifier(), + eventQuery: OCKEventQuery(for: date), + storeManager: self.storeManager, + survey: onboardSurvey.createSurvey(), + extractOutcome: onboardSurvey.extractAnswers) + if let carekitView = onboardCard.view as? OCKView { + carekitView.customStyle = CustomStylerKey.defaultValue + } + onboardCard.surveyDelegate = self - // Only show the tip view on the current date - if isCurrentDay { - if Calendar.current.isDate(date, inSameDayAs: Date()) { - // Add a non-CareKit view into the list - let tipTitle = "Benefits of exercising" - let tipText = "Learn how activity can promote a healthy pregnancy." - let tipView = TipView() - tipView.headerView.titleLabel.text = tipTitle - tipView.headerView.detailLabel.text = tipText - tipView.imageView.image = UIImage(named: "exercise.jpg") - tipView.customStyle = CustomStylerKey.defaultValue - listViewController.appendView(tipView, animated: false) + listViewController.appendViewController( + onboardCard, + animated: false + ) + return + } + + let isCurrentDay = Calendar.current.isDate(date, inSameDayAs: Date()) + + // Only show the tip view on the current date + if isCurrentDay { + if Calendar.current.isDate(date, inSameDayAs: Date()) { + // Add a non-CareKit view into the list + let tipTitle = "10 Shower Tips" + // let tipText = "Learn how activity can promote a healthy pregnancy." + // xTODO: 5 - Need to use correct initializer instead of setting properties + let customFeaturedView = CustomFeaturedContentView() + customFeaturedView.url = URL(string: "https://www.oasense.com/post/top-10-summer-shower-tips") + customFeaturedView.imageView.image = UIImage(named: "showerTips") + customFeaturedView.label.text = tipTitle + customFeaturedView.label.textColor = .white + customFeaturedView.customStyle = CustomStylerKey.defaultValue + listViewController.appendView(customFeaturedView, animated: false) + } } - } - Task { let tasks = await self.fetchTasks(on: date) tasks.compactMap { let cards = self.taskViewController(for: $0, on: date) @@ -171,24 +193,45 @@ class CareViewController: OCKDailyPageViewController { } } + // swiftlint:disable:next cyclomatic_complexity private func taskViewController(for task: OCKAnyTask, on date: Date) -> [UIViewController]? { - switch task.id { - case TaskID.steps: + let cardView: CareKitCard! + if let task = task as? OCKTask { + cardView = task.card + } else if let task = task as? OCKHealthKitTask { + cardView = task.card + } else { + return nil + } + switch cardView { + case .numericProgress: let view = NumericProgressTaskView( task: task, eventQuery: OCKEventQuery(for: date), storeManager: self.storeManager) .padding([.vertical], 20) .careKitStyle(CustomStylerKey.defaultValue) - return [view.formattedHostingController()] - case TaskID.stretch: + case .custom: + /* + xTODO: Example of showing how to use your custom card. This + should be placed correctly for the final to receive credit. + This card currently only shows when numericProgress is selected, + you should add the card to the switch statement properly to + make it show on purpose when the card type is selected. + */ + let viewModel = CustomCardViewModel(task: task, + eventQuery: .init(for: date), + storeManager: self.storeManager) + let customCard = CustomCardView(viewModel: viewModel) + return [customCard.formattedHostingController()] + case .instruction: return [OCKInstructionsTaskViewController(task: task, eventQuery: .init(for: date), storeManager: self.storeManager)] - case TaskID.kegels: + case .simple: /* Since the kegel task is only scheduled every other day, there will be cases where it is not contained in the tasks array returned from the query. @@ -198,64 +241,68 @@ class CareViewController: OCKDailyPageViewController { storeManager: self.storeManager)] // Create a card for the doxylamine task if there are events for it on this day. - case TaskID.doxylamine: + case .checklist: return [OCKChecklistTaskViewController( task: task, eventQuery: .init(for: date), storeManager: self.storeManager)] - case TaskID.nausea: - var cards = [UIViewController]() - // dynamic gradient colors - let nauseaGradientStart = UIColor { traitCollection -> UIColor in - return traitCollection.userInterfaceStyle == .light ? #colorLiteral(red: 0.06253327429, green: 0.6597633362, blue: 0.8644603491, alpha: 1) : #colorLiteral(red: 0, green: 0.2858072221, blue: 0.6897063851, alpha: 1) - } - let nauseaGradientEnd = UIColor { traitCollection -> UIColor in - return traitCollection.userInterfaceStyle == .light ? #colorLiteral(red: 0, green: 0.2858072221, blue: 0.6897063851, alpha: 1) : #colorLiteral(red: 0.06253327429, green: 0.6597633362, blue: 0.8644603491, alpha: 1) - } - - // Create a plot comparing nausea to medication adherence. - let nauseaDataSeries = OCKDataSeriesConfiguration( - taskID: "nausea", - legendTitle: "Nausea", - gradientStartColor: nauseaGradientStart, - gradientEndColor: nauseaGradientEnd, - markerSize: 10, - eventAggregator: OCKEventAggregator.countOutcomeValues) - - let doxylamineDataSeries = OCKDataSeriesConfiguration( - taskID: "doxylamine", - legendTitle: "Doxylamine", - gradientStartColor: .systemGray2, - gradientEndColor: .systemGray, - markerSize: 10, - eventAggregator: OCKEventAggregator.countOutcomeValues) - - let insightsCard = OCKCartesianChartViewController( - plotType: .bar, - selectedDate: date, - configurations: [nauseaDataSeries, doxylamineDataSeries], - storeManager: self.storeManager) - - insightsCard.chartView.headerView.titleLabel.text = "Nausea & Doxylamine Intake" - insightsCard.chartView.headerView.detailLabel.text = "This Week" - insightsCard.chartView.headerView.accessibilityLabel = "Nausea & Doxylamine Intake, This Week" - cards.append(insightsCard) - + case .button: /* Also create a card that displays a single event. The event query passed into the initializer specifies that only today's log entries should be displayed by this log task view controller. */ - let nauseaCard = OCKButtonLogTaskViewController(task: task, + let buttonCard = OCKButtonLogTaskViewController(task: task, eventQuery: .init(for: date), storeManager: self.storeManager) - cards.append(nauseaCard) - return cards + return [buttonCard] + case .labeledValue: + let view = LabeledValueTaskView( + task: task, + eventQuery: OCKEventQuery(for: date), + storeManager: self.storeManager) + .padding([.vertical], 20) + .careKitStyle(CustomStylerKey.defaultValue) + return [view.formattedHostingController()] + case .link: + let linkView = LinkView(title: .init("My Link"), + // swiftlint:disable:next line_length + links: [.website("http://www.engr.uky.edu/research-faculty/departments/computer-science", + title: "College of Engineering")]) + return [linkView.formattedHostingController()] + + case .survey: + guard let surveyTask = task as? OCKTask else { + Logger.feed.error("Can only use a survey for an \"OCKTask\", not \(task.id)") + return nil + } + let surveyTaskID = surveyTask.survey.type().identifier() + let surveyCard = OCKSurveyTaskViewController(taskID: surveyTaskID, + eventQuery: OCKEventQuery(for: date), + storeManager: self.storeManager, + survey: surveyTask.survey.type().createSurvey(), + viewSynchronizer: SurveyViewSynchronizer(), + extractOutcome: surveyTask.survey.type().extractAnswers) + surveyCard.surveyDelegate = self + return [surveyCard] default: - return nil + // Check if a healthKit task + guard task is OCKHealthKitTask else { + return [OCKSimpleTaskViewController(task: task, + eventQuery: .init(for: date), + storeManager: self.storeManager)] + } + let view = LabeledValueTaskView( + task: task, + eventQuery: OCKEventQuery(for: date), + storeManager: self.storeManager) + .padding([.vertical], 20) + .careKitStyle(CustomStylerKey.defaultValue) + + return [view.formattedHostingController()] } } @@ -264,14 +311,43 @@ class CareViewController: OCKDailyPageViewController { query.excludesTasksWithNoEvents = true do { let tasks = try await storeManager.store.fetchAnyTasks(query: query) - let orderedTasks = TaskID.ordered.compactMap { orderedTaskID in - tasks.first(where: { $0.id == orderedTaskID }) } - return orderedTasks + // Remove onboarding tasks from array + return tasks.filter { $0.id != Onboard.identifier() } } catch { Logger.feed.error("\(error.localizedDescription, privacy: .public)") return [] } } + + @MainActor + private func checkIfOnboardingIsComplete() async -> Bool { + var query = OCKOutcomeQuery() + query.taskIDs = [Onboard.identifier()] + + guard let store = AppDelegateKey.defaultValue?.store else { + Logger.feed.error("CareKit store could not be unwrapped") + return false + } + + do { + let outcomes = try await store.fetchAnyOutcomes(query: query) + return !outcomes.isEmpty + } catch { + return false + } + } +} + +extension CareViewController: OCKSurveyTaskViewControllerDelegate { + func surveyTask( + viewController: OCKSurveyTaskViewController, + for task: OCKAnyTask, + didFinish result: Result) { + + if case let .success(reason) = result, reason == .completed { + reload() + } + } } private extension View { diff --git a/OCKSample/Main/Care/CustomCards/CardViewModel.swift b/OCKSample/Main/Care/CustomCards/CardViewModel.swift new file mode 100644 index 0000000..d6454c8 --- /dev/null +++ b/OCKSample/Main/Care/CustomCards/CardViewModel.swift @@ -0,0 +1,86 @@ +// +// CardViewModel.swift +// OCKSample +// +// Created by Corey Baker on 12/3/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import CareKit +import CareKitStore +import Foundation + +/** + A basic view model that can be subclassed to make more intricate view models for custom + CareKit cards. + */ +class CardViewModel: OCKTaskController { + + // MARK: Public read/write properties + /// The error encountered by the view model. + @Published public var actionError: Error? + + // MARK: Public read private write properties + private(set) var query: SynchronizedTaskQuery? + + /// Create an instance for the default content. The first event that matches the + /// provided query will be fetched from the the store and + /// published to the view. The view will update when changes occur in the store. + /// - Parameters: + /// - taskID: The ID of the task to fetch. + /// - eventQuery: A query used to fetch an event in the store. + /// - storeManager: Wraps the store that contains the event to fetch. + convenience init(taskID: String, + eventQuery: OCKEventQuery, + storeManager: OCKSynchronizedStoreManager) { + self.init(storeManager: storeManager) + setQuery(.taskIDs([taskID], eventQuery)) + self.query?.perform(using: self) + } + + /// Create an instance for the default content. The first event that matches the + /// provided query will be fetched from the the store and + /// published to the view. The view will update when changes occur in the store. + /// - Parameters: + /// - task: The task associated with the event to fetch. + /// - eventQuery: A query used to fetch an event in the store. + /// - storeManager: Wraps the store that contains the event to fetch. + convenience init(task: OCKAnyTask, + eventQuery: OCKEventQuery, + storeManager: OCKSynchronizedStoreManager) { + self.init(storeManager: storeManager) + setQuery(.tasks([task], eventQuery)) + self.query?.perform(using: self) + } + + /** + Set the query property for this class. + - parameter query: The query to keep in sync with this view model. + */ + func setQuery(_ query: SynchronizedTaskQuery) { + self.query = query + } +} + +extension CardViewModel { + /// Creates a query that can be used to synchronize `CardViewModel`'s. + enum SynchronizedTaskQuery { + + case taskQuery(_ taskQuery: OCKTaskQuery, _ eventQuery: OCKEventQuery) + case taskIDs(_ taskIDs: [String], _ eventQuery: OCKEventQuery) + + static func tasks(_ tasks: [OCKAnyTask], _ eventQuery: OCKEventQuery) -> Self { + let taskIDs = Array(Set(tasks.map { $0.id })) + return .taskIDs(taskIDs, eventQuery) + } + + func perform(using viewModel: CardViewModel) { + switch self { + case let .taskQuery(taskQuery, eventQuery): + viewModel.fetchAndObserveEvents(forTaskQuery: taskQuery, eventQuery: eventQuery) + case let .taskIDs(taskIDs, eventQuery): + viewModel.fetchAndObserveEvents(forTaskIDs: taskIDs, eventQuery: eventQuery) + } + } + } +} diff --git a/OCKSample/Main/Care/CustomCards/CustomCard/CustomCardView.swift b/OCKSample/Main/Care/CustomCards/CustomCard/CustomCardView.swift new file mode 100644 index 0000000..0432a94 --- /dev/null +++ b/OCKSample/Main/Care/CustomCards/CustomCard/CustomCardView.swift @@ -0,0 +1,89 @@ +// +// CustomCardView.swift +// OCKSample +// +// Created by Corey Baker on 12/3/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import SwiftUI +import CareKitUI +import CareKitStore + +struct CustomCardView: View { + @Environment(\.careKitStyle) var style + @StateObject var viewModel: CustomCardViewModel + + var body: some View { + CardView { + VStack(alignment: .leading, + spacing: style.dimension.directionalInsets1.top) { + // Can look through HeaderView for creating custom + HeaderView(title: Text(viewModel.taskEvents.firstEventTitle), + detail: Text(viewModel.taskEvents.firstEventDetail ?? "")) + Divider() + HStack(alignment: .center, + spacing: style.dimension.directionalInsets2.trailing) { + + Button(action: { + Task { + await viewModel.action(viewModel.value) + } + }) { + CircularCompletionView(isComplete: viewModel.taskEvents.isFirstEventComplete) { + Image(systemName: "checkmark") // Can place any view type here + .resizable() + .padding() + .frame(width: 50, height: 50) // Change size to make larger/smaller + } + } + Spacer() + + Text("Input: ") + .font(Font.headline) + TextField("0.0", + value: $viewModel.value, + formatter: viewModel.amountFormatter) + .keyboardType(.decimalPad) + .font(Font.title.weight(.bold)) + .foregroundColor(.accentColor) + + Spacer() + Button(action: { + Task { + await viewModel.action(viewModel.value) + } + }) { + RectangularCompletionView(isComplete: viewModel.taskEvents.isFirstEventComplete) { + Image(systemName: "checkmark") // Can place any view type here + .resizable() + .padding() + .frame(width: 50, height: 50) // Change size to make larger/smaller + } + } + + Text(viewModel.valueForButton) + .multilineTextAlignment(.trailing) + .font(Font.title.weight(.bold)) + .foregroundColor(.accentColor) + } + } + .padding() + } + .onReceive(viewModel.$taskEvents) { taskEvents in + /* + DO NOT CHANGE THIS. The viewModel needs help + from view to update "value" since taskEvents + can't be overriden in viewModel. + */ + viewModel.checkIfValueShouldUpdate(taskEvents) + } + } +} + +struct CustomCardView_Previews: PreviewProvider { + static var previews: some View { + CustomCardView(viewModel: .init(storeManager: .init(wrapping: OCKStore(name: Constants.noCareStoreName, + type: .inMemory)))) + } +} diff --git a/OCKSample/Main/Care/CustomCards/CustomCard/CustomCardViewModel.swift b/OCKSample/Main/Care/CustomCards/CustomCard/CustomCardViewModel.swift new file mode 100644 index 0000000..de3e4f0 --- /dev/null +++ b/OCKSample/Main/Care/CustomCards/CustomCard/CustomCardViewModel.swift @@ -0,0 +1,90 @@ +// +// CustomCardViewModel.swift +// OCKSample +// +// Created by Corey Baker on 12/3/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import CareKit +import CareKitStore +import Foundation + +class CustomCardViewModel: CardViewModel { + /* + xTODO: Place any additional properties needed for your custom Card. + Be sure to @Published them if they update your view + */ + + /// Example value + @Published var value: Double = 0 + + let amountFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.zeroSymbol = "" + return formatter + }() + + /// This value can be used directly in Text() views. + var valueForButton: String { + guard let doubleValue = taskEvents.firstEventOutcomeValueDouble else { + return "\(Int(value))" + } + return "\(Int(doubleValue))" + } + + /// Action performed when button is tapped + private(set) var action: (Double) async -> Void = { _ in } + + /// Create an instance for the default content. The first event that matches the + /// provided query will be fetched from the the store and + /// published to the view. The view will update when changes occur in the store. + /// - Parameters: + /// - taskID: The ID of the task to fetch. + /// - eventQuery: A query used to fetch an event in the store. + /// - storeManager: Wraps the store that contains the event to fetch. + convenience init(taskID: String, + eventQuery: OCKEventQuery, + storeManager: OCKSynchronizedStoreManager) { + self.init(storeManager: storeManager) + setQuery(.taskIDs([taskID], eventQuery)) + self.query?.perform(using: self) + } + + /// Create an instance for the default content. The first event that matches the + /// provided query will be fetched from the the store and + /// published to the view. The view will update when changes occur in the store. + /// - Parameters: + /// - task: The task associated with the event to fetch. + /// - eventQuery: A query used to fetch an event in the store. + /// - storeManager: Wraps the store that contains the event to fetch. + convenience init(task: OCKAnyTask, + eventQuery: OCKEventQuery, + storeManager: OCKSynchronizedStoreManager) { + self.init(storeManager: storeManager) + setQuery(.tasks([task], eventQuery)) + self.action = { value in + do { + if self.taskEvents.firstEventOutcomeValues != nil { + _ = try await self.appendOutcomeValue(value: value, + at: .init(row: 0, section: 0)) + } else { + _ = try await self.saveOutcomesForEvent(atIndexPath: .init(row: 0, section: 0), + values: [.init(value)]) + } + } catch { + self.actionError = error + } + } + self.query?.perform(using: self) + } + + /// Automatically updates the value after it's saved to the database. + @MainActor + func checkIfValueShouldUpdate(_ updatedEvents: OCKTaskEvents) { + if let changedValue = updatedEvents.firstEventOutcomeValueDouble, + self.value != changedValue { + self.value = changedValue + } + } +} diff --git a/OCKSample/Main/Care/CustomCards/CustomFeaturedContentView.swift b/OCKSample/Main/Care/CustomCards/CustomFeaturedContentView.swift new file mode 100755 index 0000000..766fc6e --- /dev/null +++ b/OCKSample/Main/Care/CustomCards/CustomFeaturedContentView.swift @@ -0,0 +1,44 @@ +// +// CustomFeaturedContentView.swift +// OCKSample +// +// Created by Corey Baker on 11/29/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import UIKit +import CareKit +import CareKitUI + +/// A simple subclass to take control of what CareKit already gives us. +class CustomFeaturedContentView: OCKFeaturedContentView { + var url: URL? + + // Need to override so we can become delegate when the user taps on card + override init(imageOverlayStyle: UIUserInterfaceStyle = .unspecified) { + // See that this always calls the super + super.init(imageOverlayStyle: imageOverlayStyle) + self.delegate = self + } + + // A convenience initializer to make it easier to use our custom featured content + convenience init(url: String, imageOverlayStyle: UIUserInterfaceStyle = .unspecified) { + self.init(imageOverlayStyle: imageOverlayStyle) + self.url = URL(string: url) + self.delegate = self + } +} + +/// Need to conform to delegate in order to be delegated to. +extension CustomFeaturedContentView: OCKFeaturedContentViewDelegate { + + func didTapView(_ view: OCKFeaturedContentView) { + // When tapped open a URL. + guard let url = url else { + return + } + DispatchQueue.main.async { + UIApplication.shared.open(url) + } + } +} diff --git a/OCKSample/Main/Care/CustomCards/SurveyViewSynchronizer.swift b/OCKSample/Main/Care/CustomCards/SurveyViewSynchronizer.swift new file mode 100755 index 0000000..b161e32 --- /dev/null +++ b/OCKSample/Main/Care/CustomCards/SurveyViewSynchronizer.swift @@ -0,0 +1,44 @@ +// +// SurveyViewSynchronizer.swift +// OCKSample +// +// Created by Corey Baker on 11/11/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// +import CareKit +import CareKitStore +import CareKitUI +import ResearchKit +import UIKit +import os.log + +final class SurveyViewSynchronizer: OCKSurveyTaskViewSynchronizer { + + override func updateView( + _ view: OCKInstructionsTaskView, + context: OCKSynchronizationContext) { + + super.updateView(view, context: context) + + if let event = context.viewModel.first?.first, event.outcome != nil { + view.instructionsLabel.isHidden = false + /* + xTODO: You need to modify this so the instuction label shows + correctly for each Task/Card. + Hint - Each event (OCKAnyEvent) has a task. How can you use + this task to determine what instruction answers should show? + Look at how the CareViewController differentiates between + surveys. + */ + let pain = event.answer(kind: CheckIn.painItemIdentifier) + let sleep = event.answer(kind: CheckIn.sleepItemIdentifier) + + view.instructionsLabel.text = """ + Shower length: \(Int(pain)) minutes + Shampoo used: \(Int(sleep)) fluid ounces + """ + } else { + view.instructionsLabel.isHidden = true + } + } +} diff --git a/OCKSample/Main/Care/CustomCards/TipView.swift b/OCKSample/Main/Care/CustomCards/TipView.swift old mode 100644 new mode 100755 diff --git a/OCKSample/Main/Contact/ContactView.swift b/OCKSample/Main/Contact/ContactView.swift old mode 100644 new mode 100755 index 7eef2c5..75d4476 --- a/OCKSample/Main/Contact/ContactView.swift +++ b/OCKSample/Main/Contact/ContactView.swift @@ -5,7 +5,6 @@ // Created by Corey Baker on 11/25/20. // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. // - import SwiftUI import UIKit import CareKit @@ -16,7 +15,7 @@ struct ContactView: UIViewControllerRepresentable { @State var storeManager = StoreManagerKey.defaultValue func makeUIViewController(context: Context) -> some UIViewController { - let viewController = OCKContactsListViewController(storeManager: storeManager) + let viewController = CustomContactViewController(storeManager: storeManager) return UINavigationController(rootViewController: viewController) } diff --git a/OCKSample/Main/Contact/CustomContactViewController.swift b/OCKSample/Main/Contact/CustomContactViewController.swift new file mode 100755 index 0000000..4e4af50 --- /dev/null +++ b/OCKSample/Main/Contact/CustomContactViewController.swift @@ -0,0 +1,249 @@ +// +// CustomContactViewController.swift +// OCKSample +// +// Created by Corey Baker on 11/7/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// +import UIKit +import CareKitStore +import CareKit +import Contacts +import ContactsUI +import ParseSwift +import ParseCareKit +import os.log + +class CustomContactViewController: OCKListViewController { + + fileprivate weak var contactDelegate: OCKContactViewControllerDelegate? + fileprivate var allContacts = [OCKAnyContact]() + + /// The manager of the `Store` from which the `Contact` data is fetched. + public let storeManager: OCKSynchronizedStoreManager + + /// Initialize using a store manager. All of the contacts in the store manager will be queried and dispalyed. + /// + /// - Parameters: + /// - storeManager: The store manager owning the store whose contacts should be displayed. + public init(storeManager: OCKSynchronizedStoreManager) { + self.storeManager = storeManager + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + let searchController = UISearchController(searchResultsController: nil) + searchController.searchBar.searchBarStyle = UISearchBar.Style.prominent + searchController.searchBar.placeholder = " Search Contacts" + searchController.searchBar.showsCancelButton = true + searchController.searchBar.delegate = self + navigationItem.searchController = searchController + definesPresentationContext = true + + navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, + target: self, + action: #selector(presentContactsListViewController)) + + Task { + try? await fetchContacts() + } + } + + override func viewDidAppear(_ animated: Bool) { + Task { + try? await fetchContacts() + } + } + + @objc private func presentContactsListViewController() { + + let contactPicker = CNContactPickerViewController() + contactPicker.delegate = self + contactPicker.predicateForEnablingContact = NSPredicate( + format: "phoneNumbers.@count > 0") + present(contactPicker, animated: true, completion: nil) + } + + @objc private func dismissViewController() { + dismiss(animated: true, completion: nil) + } + + func clearAndKeepSearchBar() { + clear() + } + + @MainActor + func fetchContacts() async throws { + guard User.current != nil else { + Logger.contact.error("User not logged in") + return + } + + var query = OCKContactQuery(for: Date()) + query.sortDescriptors.append(.familyName(ascending: true)) + query.sortDescriptors.append(.givenName(ascending: true)) + + let contacts = try await storeManager.store.fetchAnyContacts(query: query) + + guard let convertedContacts = contacts as? [OCKContact], + let personUUIDString = try? Utility.getRemoteClockUUID().uuidString else { + Logger.contact.error("Could not convert contacts") + return + } + + // xTODO: Modify this filter to not show the contact info for this user + let filterdContacts = convertedContacts.filter { convertedContact in + Logger.contact.info("Contact filtered: \(convertedContact.id)") + return convertedContact.id != personUUIDString + } + + self.clearAndKeepSearchBar() + self.allContacts = filterdContacts + self.displayContacts(self.allContacts) + } + + @MainActor + func displayContacts(_ contacts: [OCKAnyContact]) { + for contact in contacts { + let contactViewController = OCKSimpleContactViewController(contact: contact, + storeManager: storeManager) + contactViewController.delegate = self.contactDelegate + self.appendViewController(contactViewController, animated: false) + } + } + + func convertDeviceContacts(_ contact: CNContact) -> OCKAnyContact { + + var convertedContact = OCKContact(id: contact.identifier, givenName: contact.givenName, + familyName: contact.familyName, carePlanUUID: nil) + convertedContact.title = contact.jobTitle + + var emails = [OCKLabeledValue]() + contact.emailAddresses.forEach { + emails.append(OCKLabeledValue(label: $0.label ?? "email", value: $0.value as String)) + } + convertedContact.emailAddresses = emails + + var phoneNumbers = [OCKLabeledValue]() + contact.phoneNumbers.forEach { + phoneNumbers.append(OCKLabeledValue(label: $0.label ?? "phone", value: $0.value.stringValue)) + } + convertedContact.phoneNumbers = phoneNumbers + convertedContact.messagingNumbers = phoneNumbers + + if let address = contact.postalAddresses.first { + convertedContact.address = { + let newAddress = OCKPostalAddress() + newAddress.street = address.value.street + newAddress.city = address.value.city + newAddress.state = address.value.state + newAddress.postalCode = address.value.postalCode + return newAddress + }() + } + + return convertedContact + } +} + +extension CustomContactViewController: UISearchBarDelegate { + func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + Logger.contact.debug("Searching text is '\(searchText)'") + + if searchBar.text!.isEmpty { + // Show all contacts + clearAndKeepSearchBar() + displayContacts(allContacts) + return + } + + clearAndKeepSearchBar() + + let filteredContacts = allContacts.filter { (contact: OCKAnyContact) -> Bool in + + if let givenName = contact.name.givenName { + return givenName.lowercased().contains(searchText.lowercased()) + } else if let familyName = contact.name.familyName { + return familyName.lowercased().contains(searchText.lowercased()) + } else { + return false + } + } + displayContacts(filteredContacts) + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + clearAndKeepSearchBar() + displayContacts(allContacts) + } +} + +extension CustomContactViewController: OCKContactViewControllerDelegate { + + // swiftlint:disable:next line_length + func contactViewController(_ viewController: CareKit.OCKContactViewController, didEncounterError error: Error) where C: CareKit.OCKContactController, VS: CareKit.OCKContactViewSynchronizerProtocol { + Logger.contact.error("\(error.localizedDescription)") + } +} + +extension CustomContactViewController: CNContactPickerDelegate { + + @MainActor + func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) { + guard User.current != nil else { + Logger.contact.error("User not logged in") + return + } + + let contactToAdd = convertDeviceContacts(contact) + + if !(self.allContacts.contains { $0.id == contactToAdd.id }) { + + // Note - once the functionality is added to edit a contact, + // let the user potentially edit before the save + Task { + do { + _ = try await storeManager.store.addAnyContact(contactToAdd) + try? await self.fetchContacts() + } catch { + Logger.contact.error("Could not add contact: \(error.localizedDescription)") + } + } + } + } + + @MainActor + func contactPicker(_ picker: CNContactPickerViewController, didSelect contacts: [CNContact]) { + guard User.current != nil else { + Logger.contact.error("User not logged in") + return + } + + let newContacts = contacts.compactMap { convertDeviceContacts($0) } + + var contactsToAdd = [OCKAnyContact]() + for newContact in newContacts { + // swiftlint:disable:next for_where + if self.allContacts.first(where: { $0.id == newContact.id }) == nil { + contactsToAdd.append(newContact) + } + } + + let immutableContactsToAdd = contactsToAdd + Task { + do { + _ = try await storeManager.store.addAnyContacts(immutableContactsToAdd) + try? await self.fetchContacts() + } catch { + Logger.contact.error("Could not add contacts: \(error.localizedDescription)") + } + } + } +} diff --git a/OCKSample/Main/Insights/InsightsView.swift b/OCKSample/Main/Insights/InsightsView.swift new file mode 100644 index 0000000..a429828 --- /dev/null +++ b/OCKSample/Main/Insights/InsightsView.swift @@ -0,0 +1,27 @@ +// +// InsightsView.swift +// OCKSample +// +// Created by Corey Baker on 12/5/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// +import SwiftUI + +struct InsightsView: UIViewControllerRepresentable { + @State var storeManager = StoreManagerKey.defaultValue + + func makeUIViewController(context: Context) -> some UIViewController { + let viewController = InsightsViewController(storeManager: storeManager) + let navigationController = UINavigationController(rootViewController: viewController) + return navigationController + } + + func updateUIViewController(_ uiViewController: UIViewControllerType, + context: Context) {} +} + +struct InsightsView_Previews: PreviewProvider { + static var previews: some View { + InsightsView() + } +} diff --git a/OCKSample/Main/Insights/InsightsViewController.swift b/OCKSample/Main/Insights/InsightsViewController.swift new file mode 100644 index 0000000..eee4656 --- /dev/null +++ b/OCKSample/Main/Insights/InsightsViewController.swift @@ -0,0 +1,215 @@ +// +// InsightsViewController.swift +// OCKSample +// +// Created by Corey Baker on 12/5/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// +/* + You should notice this looks like CareViewController and + MyContactViewController combined, + but only shows charts instead. +*/ + +import UIKit +import CareKitStore +import CareKitUI +import CareKit +import ParseSwift +import ParseCareKit +import os.log + +class InsightsViewController: OCKListViewController { + + /// The manager of the `Store` from which the `Contact` data is fetched. + public let storeManager: OCKSynchronizedStoreManager + + /// Initialize using a store manager. All of the contacts in the store manager will be queried and dispalyed. + /// + /// - Parameters: + /// - storeManager: The store manager owning the store whose contacts should be displayed. + public init(storeManager: OCKSynchronizedStoreManager) { + self.storeManager = storeManager + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.title = "Insights" + + Task { + await displayTasks(Date()) + } + } + + override func viewDidAppear(_ animated: Bool) { + Task { + await displayTasks(Date()) + } + } + + override func appendViewController(_ viewController: UIViewController, animated: Bool) { + super.appendViewController(viewController, animated: animated) + + // Make sure this contact card matches app style when possible + if let carekitView = viewController.view as? OCKView { + carekitView.customStyle = CustomStylerKey.defaultValue + } + } + + @MainActor + func fetchTasks(on date: Date) async -> [OCKAnyTask] { + /** + xTODO: How would you modify this to fetch all of your tasks? + Hint - you should look at the same function in CareViewController. If you + understand queries and filters, this will be straightforward. + */ + var query = OCKTaskQuery(for: date) + query.excludesTasksWithNoEvents = true + do { + let tasks = try await storeManager.store.fetchAnyTasks(query: query) + var taskIDs = TaskID.ordered + taskIDs.append(CheckIn().identifier()) + let orderedTasks = taskIDs.compactMap { orderedTaskID in + tasks.first(where: { $0.id == orderedTaskID }) } + return orderedTasks + } catch { + Logger.insights.error("\(error.localizedDescription, privacy: .public)") + return [] + } + } + + /* + xTODO: Plot all of your tasks in this method. Note that you can combine multiple + tasks into one chart (like the Nausea/Doxlymine chart if they are related. + */ + + func taskViewController(for task: OCKAnyTask, + on date: Date) -> [UIViewController]? { + /* + xTODO: CareKit has 3 plotType's: .bar, .scatter, and .line. + You should have a 3 types in your InsightView meaning you + should have at least 3 charts. Remember that all of your + tasks need to be graphed so you may have more. The solution + for not this should not be to show all 3 plot types for a + single task. Your code should be flexible enough to determine + a graph type. Instead, you should look extend OCKTask and OCKAnyTask + to add a "graph" property similar to "card". This means you probably + should create a "GraphCard" enum similar to "CareKitCard" and allow + the user to select the specific graph when adding a new task. + Hint - you should look at the same function in CareViewController + to determine how to switch graphs on an enum. + */ + + let survey = CheckIn() // Only used for example. + let surveyTaskID = survey.identifier() // Only used for example. + switch task.id { + case surveyTaskID: + + /* + Note that that there's a small bug for the check in graph because + it averages all of the "Pain + Sleep" hours. This okay for now. If + you are collecting ResearchKit input that only collects 1 value per + survey, you won't have this problem. + */ + + // dynamic gradient colors + let meanGradientStart = TintColorFlipKey.defaultValue + let meanGradientEnd = TintColorKey.defaultValue + + // Create a plot comparing mean to median. + let meanDataSeries = OCKDataSeriesConfiguration( + taskID: surveyTaskID, + legendTitle: "Mean", + gradientStartColor: meanGradientStart, + gradientEndColor: meanGradientEnd, + markerSize: 10, + eventAggregator: .aggregatorMean(CheckIn.sleepItemIdentifier)) + + let medianDataSeries = OCKDataSeriesConfiguration( + taskID: surveyTaskID, + legendTitle: "Median", + gradientStartColor: .systemGray2, + gradientEndColor: .systemGray, + markerSize: 10, + eventAggregator: .aggregatorMedian(CheckIn.sleepItemIdentifier)) + + let insightsCard = OCKCartesianChartViewController( + plotType: .line, + selectedDate: date, + configurations: [meanDataSeries, medianDataSeries], + storeManager: self.storeManager) + + insightsCard.chartView.headerView.titleLabel.text = "Shampoo used" + insightsCard.chartView.headerView.detailLabel.text = "This Week" + insightsCard.chartView.headerView.accessibilityLabel = "Mean & Median, This Week" + + return [insightsCard] + + case TaskID.nausea: + var cards = [UIViewController]() + // dynamic gradient colors + let nauseaGradientStart = TintColorFlipKey.defaultValue + let nauseaGradientEnd = TintColorKey.defaultValue + + // Create a plot comparing nausea to medication adherence. + let nauseaDataSeries = OCKDataSeriesConfiguration( + taskID: TaskID.nausea, + legendTitle: "Dirtiness", + gradientStartColor: nauseaGradientStart, + gradientEndColor: nauseaGradientEnd, + markerSize: 10, + eventAggregator: OCKEventAggregator.countOutcomeValues) + + let doxylamineDataSeries = OCKDataSeriesConfiguration( + taskID: TaskID.washFace, + legendTitle: "Face washes", + gradientStartColor: .systemGray2, + gradientEndColor: .systemGray, + markerSize: 10, + eventAggregator: OCKEventAggregator.countOutcomeValues) + + let insightsCard = OCKCartesianChartViewController( + plotType: .bar, + selectedDate: date, + configurations: [nauseaDataSeries, doxylamineDataSeries], + storeManager: self.storeManager) + + insightsCard.chartView.headerView.titleLabel.text = "Diritness and Face Washings" + insightsCard.chartView.headerView.detailLabel.text = "This Week" + insightsCard.chartView.headerView.accessibilityLabel = "Nausea & Doxylamine Intake, This Week" + cards.append(insightsCard) + + return cards + + default: + return nil + } + } + + @MainActor + func displayTasks(_ date: Date) async { + + let tasks = await fetchTasks(on: date) + self.clear() // Clear after pulling tasks from database + tasks.compactMap { + let cards = self.taskViewController(for: $0, on: date) + cards?.forEach { + if let carekitView = $0.view as? OCKView { + carekitView.customStyle = CustomStylerKey.defaultValue + } + } + return cards + }.forEach { (cards: [UIViewController]) in + cards.forEach { + self.appendViewController($0, animated: false) + } + } + } +} diff --git a/OCKSample/Main/Login/LoginView.swift b/OCKSample/Main/Login/LoginView.swift old mode 100644 new mode 100755 index c182c5b..5cb16d7 --- a/OCKSample/Main/Login/LoginView.swift +++ b/OCKSample/Main/Login/LoginView.swift @@ -26,6 +26,7 @@ struct LoginView: View { @ObservedObject var viewModel: LoginViewModel @State var usersname = "" @State var password = "" + @State var email = "" @State var firstName: String = "" @State var lastName: String = "" @State var signupLoginSegmentValue = 0 @@ -33,16 +34,17 @@ struct LoginView: View { var body: some View { VStack { // Change the title to the name of your application - Text("CareKit Sample App") + Text("Shower Tracker") .font(.largeTitle) + .fontWeight(.bold) .foregroundColor(.white) .padding() // Change this image to something that represents your application - Image("exercise.jpg") + Image("shower") .resizable() .frame(width: 150, height: 150, alignment: .center) .clipShape(Circle()) - .overlay(Circle().stroke(Color(.white), lineWidth: 4)) + .overlay(Circle().stroke(Color(.purple), lineWidth: 4)) .shadow(radius: 10) .padding() @@ -71,6 +73,11 @@ struct LoginView: View { .background(.white) .cornerRadius(20.0) .shadow(radius: 10.0, x: 20, y: 10) + TextField("Email", text: $email) + .padding() + .background(.white) + .cornerRadius(20.0) + .shadow(radius: 10.0, x: 20, y: 10) switch signupLoginSegmentValue { case 1: @@ -102,6 +109,7 @@ struct LoginView: View { await viewModel.signup(.patient, username: usersname, password: password, + email: email, firstName: firstName, lastName: lastName) } @@ -127,7 +135,7 @@ struct LoginView: View { .frame(width: 300) } }) - .background(Color(.green)) + .background(Color(.purple)) .cornerRadius(15) Button(action: { diff --git a/OCKSample/Main/Login/LoginViewModel.swift b/OCKSample/Main/Login/LoginViewModel.swift old mode 100644 new mode 100755 index 7b680b6..4714925 --- a/OCKSample/Main/Login/LoginViewModel.swift +++ b/OCKSample/Main/Login/LoginViewModel.swift @@ -117,8 +117,16 @@ class LoginViewModel: ObservableObject { throw AppError.couldntCast } - try await appDelegate.store?.populateSampleData() - try await appDelegate.healthKitStore.populateSampleData() + // Added code to create a contact for the respective signed up user + let newContact = OCKContact(id: remoteUUID.uuidString, + name: newPatient.name, + carePlanUUID: nil) + + // This is new contact that has never been saved before + _ = try await storeManager.store.addAnyContact(newContact) + + try await appDelegate.store?.populateSampleData(patient.uuid) + // try await appDelegate.healthKitStore.populateSampleData(patient.uuid) appDelegate.parseRemote.automaticallySynchronizes = true // Post notification to sync @@ -141,6 +149,7 @@ class LoginViewModel: ObservableObject { func signup(_ type: UserType, username: String, password: String, + email: String? = nil, firstName: String, lastName: String) async { do { @@ -152,6 +161,7 @@ class LoginViewModel: ObservableObject { // Set any properties you want saved on the user befor logging in. newUser.username = username.lowercased() newUser.password = password + newUser.email = email let user = try await newUser.signup() Logger.login.info("Parse signup successful: \(user)") let patient = try await savePatientAfterSignUp(type, diff --git a/OCKSample/Main/MainTabView.swift b/OCKSample/Main/MainTabView.swift old mode 100644 new mode 100755 index d33625b..2025da4 --- a/OCKSample/Main/MainTabView.swift +++ b/OCKSample/Main/MainTabView.swift @@ -7,7 +7,6 @@ // // swiftlint:disable:next line_length // This was built using tutorial: https://www.hackingwithswift.com/books/ios-swiftui/creating-tabs-with-tabview-and-tabitem - import SwiftUI struct MainTabView: View { @@ -28,21 +27,33 @@ struct MainTabView: View { } .tag(0) - ContactView() + InsightsView() .tabItem { if selectedTab == 1 { - Image("phone.bubble.left.fill") + Image(systemName: "chart.pie.fill") .renderingMode(.template) } else { - Image("phone.bubble.left") + Image(systemName: "chart.pie") .renderingMode(.template) } } .tag(1) - ProfileView(loginViewModel: loginViewModel) + ContactView() .tabItem { if selectedTab == 2 { + Image(systemName: "phone.bubble.left.fill") + .renderingMode(.template) + } else { + Image(systemName: "phone.bubble.left") + .renderingMode(.template) + } + } + .tag(2) + + ProfileView(loginViewModel: loginViewModel) + .tabItem { + if selectedTab == 3 { Image("connect-filled") .renderingMode(.template) } else { @@ -50,7 +61,7 @@ struct MainTabView: View { .renderingMode(.template) } } - .tag(2) + .tag(3) } .navigationBarHidden(true) } diff --git a/OCKSample/Main/MainView.swift b/OCKSample/Main/MainView.swift old mode 100644 new mode 100755 diff --git a/OCKSample/Main/Onboarding/Consent.swift b/OCKSample/Main/Onboarding/Consent.swift new file mode 100755 index 0000000..216fe08 --- /dev/null +++ b/OCKSample/Main/Onboarding/Consent.swift @@ -0,0 +1,55 @@ +// +// Consent.swift +// OCKSample +// +// Created by Corey Baker on 11/11/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation + +// swiftlint:disable line_length + +/* + xTODO: The informedConsentHTML property allows you to display HTML + on an ResearchKit Survey. Modify the consent so it properly + represents the usecase of your application. + */ + +let informedConsentHTML = """ + + + + + + + + +

Informed Consent

+

Study Expectations

+
    +
  • You will be asked if your shower data can be shared.
  • +
  • You will be sent notifications to complete surveys.
  • +
  • You will provide reliable data i.e. water usage, shower length, etc.
  • +
  • The study is expected to last 2 years.
  • +
  • You will not be compensated for this study.
  • +
  • Your information will be kept private.
  • +
  • You can opt out of this study any time.
  • +
+

Eligibility Requirements

+
    +
  • Must be 18 years or older or have parental consent.
  • +
  • Must be able to read and understand English.
  • +
  • Must be the only user of the device on which you are participating in the study.
  • +
  • Must be able to sign your own consent form.
  • +
+

By signing below, I acknowledge that I have read this consent carefully, that I understand all of its terms, and that I enter into this study voluntarily. I understand that my information will only be used and disclosed for the purposes described in the consent and I can never withdraw from the study at any time.

+

Please sign using your finger below.

+
+ + + """ diff --git a/OCKSample/Main/Profile/HealthKitTaskView.swift b/OCKSample/Main/Profile/HealthKitTaskView.swift new file mode 100755 index 0000000..8753f42 --- /dev/null +++ b/OCKSample/Main/Profile/HealthKitTaskView.swift @@ -0,0 +1,56 @@ +// +// HealthKitTaskView.swift +// OCKSample +// +// Created by on 10/27/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import SwiftUI +import CareKitUI + +struct HealthKitTaskView: View { + @StateObject var viewModel = HealtKitTaskViewModel() + @State var title = "" + @State var instructions = "" + @State var schedule = Date() + + var body: some View { + NavigationView { + VStack { + VStack(alignment: .leading) { + TextField("Title", text: $title) + .padding() + .cornerRadius(20.0) + .shadow(radius: 10.0, x: 20, y: 10) + + TextField("Instructions", text: $instructions) + .padding() + .cornerRadius(20.0) + .shadow(radius: 10.0, x: 20, y: 10) + + DatePicker("Schedule", selection: $schedule, displayedComponents: [DatePickerComponents.date]) + .padding() + .cornerRadius(20.0) + .shadow(radius: 10.0, x: 20, y: 10) + } + + Button(action: { + Task { + do { + await viewModel.addTask(title, instructions: instructions, schedule: schedule) + } + } + }, label: { + Text("Save HealthKitTask") + .font(.headline) + .foregroundColor(.white) + .padding() + .frame(width: 300, height: 50) + }) + .background(Color(.green)) + .cornerRadius(15) + } + } + } +} diff --git a/OCKSample/Main/Profile/HealthKitTaskViewModel.swift b/OCKSample/Main/Profile/HealthKitTaskViewModel.swift new file mode 100755 index 0000000..0795447 --- /dev/null +++ b/OCKSample/Main/Profile/HealthKitTaskViewModel.swift @@ -0,0 +1,57 @@ +// +// HealthKitTaskViewModel.swift +// OCKSample +// +// Created by on 10/27/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation +import CareKit +import CareKitStore +import SwiftUI + +class HealtKitTaskViewModel: ObservableObject { + + @Published var healthTask = OCKHealthKitTask(id: "", + title: nil, + carePlanUUID: nil, + schedule: .dailyAtTime(hour: 0, + minutes: 0, + start: Date(), + end: nil, + text: nil), + healthKitLinkage: .init(quantityIdentifier: .waterTemperature, + quantityType: .discrete, + unit: .fluidOunceUS())) + + @Published var error: AppError? + + // MARK: Intents + func addTask(_ title: String, instructions: String, schedule: Date) async { + guard AppDelegateKey.defaultValue != nil else { + error = AppError.couldntBeUnwrapped + return + } + + var updateHealthTask = OCKHealthKitTask(id: title, + title: title, + carePlanUUID: nil, + schedule: .dailyAtTime(hour: 0, + minutes: 0, + start: Date(), + end: nil, + text: nil), + healthKitLinkage: .init(quantityIdentifier: .waterTemperature, + quantityType: .discrete, + unit: .fluidOunceUS())) + + updateHealthTask.instructions = instructions + + do { + try await AppDelegateKey.defaultValue?.healthKitStore.addTasksIfNotPresent([updateHealthTask]) + } catch { + self.error = AppError.errorString("Couldn't add task: \(error.localizedDescription)") + } + } +} diff --git a/OCKSample/Main/Profile/ImagePicker.swift b/OCKSample/Main/Profile/ImagePicker.swift new file mode 100755 index 0000000..dbe8659 --- /dev/null +++ b/OCKSample/Main/Profile/ImagePicker.swift @@ -0,0 +1,50 @@ +// +// ImagePicker.swift +// OCKSample +// +// Created by Corey Baker on 11/8/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +// swiftlint:disable:next line_length +// Credit to: https://www.hackingwithswift.com/books/ios-swiftui/importing-an-image-into-swiftui-using-uiimagepickercontroller + +import SwiftUI +import UIKit + +struct ImagePicker: UIViewControllerRepresentable { + @Environment(\.presentationMode) var presentationMode + @Binding var image: UIImage? + + func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIImagePickerController { + let picker = UIImagePickerController() + picker.delegate = context.coordinator + return picker + } + + func updateUIViewController(_ uiViewController: UIImagePickerController, + context: UIViewControllerRepresentableContext) { + + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { + let parent: ImagePicker + + init(_ parent: ImagePicker) { + self.parent = parent + } + + func imagePickerController(_ picker: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + if let uiImage = info[.originalImage] as? UIImage { + parent.image = uiImage + } + + parent.presentationMode.wrappedValue.dismiss() + } + } +} diff --git a/OCKSample/Main/Profile/MyContactView.swift b/OCKSample/Main/Profile/MyContactView.swift new file mode 100755 index 0000000..38f02e2 --- /dev/null +++ b/OCKSample/Main/Profile/MyContactView.swift @@ -0,0 +1,33 @@ +// +// MyContactView.swift +// OCKSample +// +// Created by Corey Baker on 11/8/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import SwiftUI +import UIKit +import CareKit +import CareKitStore +import os.log + +struct MyContactView: UIViewControllerRepresentable { + @State var storeManager = StoreManagerKey.defaultValue + + func makeUIViewController(context: Context) -> some UIViewController { + let viewController = MyContactViewController(storeManager: storeManager) + return UINavigationController(rootViewController: viewController) + } + + func updateUIViewController(_ uiViewController: UIViewControllerType, + context: Context) {} +} + +struct MyContactView_Previews: PreviewProvider { + + static var previews: some View { + MyContactView(storeManager: Utility.createPreviewStoreManager()) + .accentColor(Color(TintColorKey.defaultValue)) + } +} diff --git a/OCKSample/Main/Profile/MyContactViewController.swift b/OCKSample/Main/Profile/MyContactViewController.swift new file mode 100755 index 0000000..f4c6dc2 --- /dev/null +++ b/OCKSample/Main/Profile/MyContactViewController.swift @@ -0,0 +1,111 @@ +// +// MyContactViewController.swift +// OCKSample +// +// Created by Corey Baker on 11/8/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import UIKit +import CareKitStore +import CareKitUI +import CareKit +import Contacts +import ContactsUI +import ParseSwift +import ParseCareKit +import os.log + +class MyContactViewController: OCKListViewController { + + fileprivate weak var contactDelegate: OCKContactViewControllerDelegate? + fileprivate var contacts = [OCKAnyContact]() + + /// The manager of the `Store` from which the `Contact` data is fetched. + public let storeManager: OCKSynchronizedStoreManager + + /// Initialize using a store manager. All of the contacts in the store manager will be queried and dispalyed. + /// + /// - Parameters: + /// - storeManager: The store manager owning the store whose contacts should be displayed. + public init(storeManager: OCKSynchronizedStoreManager) { + self.storeManager = storeManager + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + Task { + try? await fetchContacts() + } + } + + override func viewDidAppear(_ animated: Bool) { + Task { + try? await fetchContacts() + } + } + + override func appendViewController(_ viewController: UIViewController, animated: Bool) { + super.appendViewController(viewController, animated: animated) + // Make sure this contact card matches app style when possible + if let carekitView = viewController.view as? OCKView { + carekitView.customStyle = CustomStylerKey.defaultValue + } + } + + @MainActor + func fetchContacts() async throws { + + guard User.current != nil, + let personUUIDString = try? Utility.getRemoteClockUUID().uuidString else { + Logger.myContact.error("User not logged in") + self.contacts.removeAll() + return + } + + /* + xTODO: How would you modify this query to only fetch the contact that belongs to this device? + + Hint 1: There are multiple ways to do this. You can modify the query + below which can work. + + Hint2: Look at the other queries in the app related to the uuid of the + user/patient who's signed in. + + Hint3: You should have a warning currently, solving this properly would + get rid of the warning without changing the line the warning is on. + */ + var query = OCKContactQuery(for: Date()) + query.ids = [personUUIDString] + query.sortDescriptors.append(.familyName(ascending: true)) + query.sortDescriptors.append(.givenName(ascending: true)) + + self.contacts = try await storeManager.store.fetchAnyContacts(query: query) + self.displayContacts() + } + + @MainActor + func displayContacts() { + self.clear() + for contact in self.contacts { + let contactViewController = OCKDetailedContactViewController(contact: contact, + storeManager: storeManager) + contactViewController.delegate = self.contactDelegate + self.appendViewController(contactViewController, animated: false) + } + } +} + +extension MyContactViewController: OCKContactViewControllerDelegate { + + // swiftlint:disable:next line_length + func contactViewController(_ viewController: CareKit.OCKContactViewController, didEncounterError error: Error) where C: CareKit.OCKContactController, VS: CareKit.OCKContactViewSynchronizerProtocol { + Logger.myContact.error("\(error.localizedDescription)") + } +} diff --git a/OCKSample/Main/Profile/ProfileImageView.swift b/OCKSample/Main/Profile/ProfileImageView.swift new file mode 100755 index 0000000..b6ee755 --- /dev/null +++ b/OCKSample/Main/Profile/ProfileImageView.swift @@ -0,0 +1,46 @@ +// +// ProfileImageView.swift +// OCKSample +// +// Created by Corey Baker on 11/8/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import SwiftUI + +struct ProfileImageView: View { + @Environment(\.tintColor) private var tintColor + @ObservedObject var viewModel: ProfileViewModel + + var body: some View { + if let image = viewModel.profileUIImage { + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 100, height: 100, alignment: .center) + .clipShape(Circle()) + .shadow(radius: 10) + .overlay(Circle().stroke(Color(tintColor), lineWidth: 5)) + .onTapGesture { + self.viewModel.isPresentingImagePicker = true + } + } else { + Image(systemName: "person.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 100, height: 100, alignment: .center) + .clipShape(Circle()) + .shadow(radius: 10) + .overlay(Circle().stroke(Color(tintColor), lineWidth: 5)) + .onTapGesture { + self.viewModel.isPresentingImagePicker = true + } + } + } +} + +struct ProfileImageView_Previews: PreviewProvider { + static var previews: some View { + ProfileImageView(viewModel: .init()) + } +} diff --git a/OCKSample/Main/Profile/ProfileView.swift b/OCKSample/Main/Profile/ProfileView.swift old mode 100644 new mode 100755 index 6663de1..e1d4bc3 --- a/OCKSample/Main/Profile/ProfileView.swift +++ b/OCKSample/Main/Profile/ProfileView.swift @@ -13,77 +13,103 @@ import CareKit import os.log struct ProfileView: View { + @Environment(\.tintColor) private var tintColor @StateObject var viewModel = ProfileViewModel() @ObservedObject var loginViewModel: LoginViewModel - @State var firstName = "" - @State var lastName = "" - @State var birthday = Date() var body: some View { - VStack { - VStack(alignment: .leading) { - TextField("First Name", text: $firstName) - .padding() - .cornerRadius(20.0) - .shadow(radius: 10.0, x: 20, y: 10) + NavigationView { + VStack { + VStack { + ProfileImageView(viewModel: viewModel) + Form { + Section(header: Text("About")) { + TextField("First Name", text: $viewModel.firstName) + TextField("Last Name", text: $viewModel.lastName) + TextField("Allergies", text: $viewModel.allergies) + DatePicker("Birthday", + selection: $viewModel.birthday, + displayedComponents: [DatePickerComponents.date]) + Picker(selection: $viewModel.sex, + label: Text("Sex")) { + Text(OCKBiologicalSex.female.rawValue).tag(OCKBiologicalSex.female) + Text(OCKBiologicalSex.male.rawValue).tag(OCKBiologicalSex.male) + Text(viewModel.sex.rawValue) + .tag(OCKBiologicalSex.other(viewModel.sexOtherField)) + } + } + Section(header: Text("Contact")) { + TextField("Street", text: $viewModel.street) + TextField("City", text: $viewModel.city) + TextField("State", text: $viewModel.state) + TextField("Postal code", text: $viewModel.zipcode) + TextField("Email Address", text: $viewModel.email) + TextField("Messaging Numbers", text: $viewModel.messagingNumber) + TextField("Phone Numbers", text: $viewModel.phoneNumber) + TextField("Other Contact Info", text: $viewModel.otherContactInfo) + } + } + } - TextField("Last Name", text: $lastName) - .padding() - .cornerRadius(20.0) - .shadow(radius: 10.0, x: 20, y: 10) + Button(action: { + Task { + await viewModel.saveProfile() + } + }, label: { + Text("Save Profile") + .font(.headline) + .foregroundColor(.white) + .padding() + .frame(width: 300, height: 50) + }) + .background(Color(.green)) + .cornerRadius(15) - DatePicker("Birthday", selection: $birthday, displayedComponents: [DatePickerComponents.date]) - .padding() - .cornerRadius(20.0) - .shadow(radius: 10.0, x: 20, y: 10) + // Notice that "action" is a closure (which is essentially + // a function as argument like we discussed in class) + Button(action: { + Task { + await loginViewModel.logout() + } + }, label: { + Text("Log Out") + .font(.headline) + .foregroundColor(.white) + .padding() + .frame(width: 300, height: 50) + }) + .background(Color(.red)) + .cornerRadius(15) } - - Button(action: { - Task { - do { - try await viewModel.saveProfile(firstName, - last: lastName, - birth: birthday) - } catch { - Logger.profile.error("Error saving profile: \(error.localizedDescription)") + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("My Contact") { + viewModel.isPresentingContact = true + } + .sheet(isPresented: $viewModel.isPresentingContact) { + MyContactView() } } - }, label: { - Text("Save Profile") - .font(.headline) - .foregroundColor(.white) - .padding() - .frame(width: 300, height: 50) - }) - .background(Color(.green)) - .cornerRadius(15) - - // Notice that "action" is a closure (which is essentially - // a function as argument like we discussed in class) - Button(action: { - Task { - await loginViewModel.logout() + ToolbarItem(placement: .navigationBarTrailing) { + Button("Add Task") { + viewModel.isPresentingAddTask = true + } + .sheet(isPresented: $viewModel.isPresentingAddTask) { + TaskView() + } } - }, label: { - Text("Log Out") - .font(.headline) - .foregroundColor(.white) - .padding() - .frame(width: 300, height: 50) - }) - .background(Color(.red)) - .cornerRadius(15) - }.onReceive(viewModel.$patient, perform: { patient in - if let currentFirstName = patient?.name.givenName { - firstName = currentFirstName } - if let currentLastName = patient?.name.familyName { - lastName = currentLastName + .sheet(isPresented: $viewModel.isPresentingImagePicker) { + ImagePicker(image: $viewModel.profileUIImage) } - if let currentBirthday = patient?.birthday { - birthday = currentBirthday + .alert(isPresented: $viewModel.isShowingSaveAlert) { + return Alert(title: Text("Update"), + message: Text(viewModel.alertMessage), + dismissButton: .default(Text("Ok"), action: { + viewModel.isShowingSaveAlert = false + })) } - }) + } } } diff --git a/OCKSample/Main/Profile/ProfileViewModel.swift b/OCKSample/Main/Profile/ProfileViewModel.swift old mode 100644 new mode 100755 index 6d2a7dd..4465c84 --- a/OCKSample/Main/Profile/ProfileViewModel.swift +++ b/OCKSample/Main/Profile/ProfileViewModel.swift @@ -13,23 +13,92 @@ import SwiftUI import ParseCareKit import os.log import Combine +import ParseSwift class ProfileViewModel: ObservableObject { // MARK: Public read, private write properties - @Published private(set) var patient: OCKPatient? + @Published var firstName = "" + @Published var lastName = "" + @Published var birthday = Date() + @Published var sex: OCKBiologicalSex = .other("other") + @Published var sexOtherField = "other" + @Published var allergies = "" + @Published var note = "" + @Published var street = "" + @Published var city = "" + @Published var state = "" + @Published var zipcode = "" + @Published var email = "" + @Published var messagingNumber = "" + @Published var phoneNumber = "" + @Published var otherContactInfo = "" + @Published var isShowingSaveAlert = false + @Published var isPresentingAddTask = false + @Published var isPresentingContact = false + @Published var isPresentingImagePicker = false + @Published var profileUIImage = UIImage(systemName: "person.fill") { + willSet { + guard self.profileUIImage != newValue, + let inputImage = newValue else { + return + } + + if !isSettingProfilePictureForFirstTime { + Task { + guard var currentUser = User.current, + let image = inputImage.jpegData(compressionQuality: 0.25) else { + Logger.profile.error("User is not logged in or could not compress image") + return + } + + let newProfilePicture = ParseFile(name: "profile.jpg", data: image) + // Use `.set()` to update ParseObject's that have already been saved before. + currentUser = currentUser.set(\.profilePicture, to: newProfilePicture) + do { + _ = try await currentUser.save() + Logger.profile.info("Saved updated profile picture successfully.") + } catch { + Logger.profile.error("Could not save profile picture: \(error.localizedDescription)") + } + } + } + } + } + @Published private(set) var error: Error? private(set) var storeManager: OCKSynchronizedStoreManager + private(set) var alertMessage = "All changs saved successfully!" // MARK: Private read/write properties + private var patient: OCKPatient? { + willSet { + if let currentFirstName = newValue?.name.givenName { + firstName = currentFirstName + } else { + firstName = "" + } + if let currentLastName = newValue?.name.familyName { + lastName = currentLastName + } else { + lastName = "" + } + if let currentBirthday = newValue?.birthday { + birthday = currentBirthday + } else { + birthday = Date() + } + } + } + private var contact: OCKContact? + private var isSettingProfilePictureForFirstTime = true private var cancellables: Set = [] init(storeManager: OCKSynchronizedStoreManager? = nil) { self.storeManager = storeManager ?? StoreManagerKey.defaultValue reloadViewModel() - NotificationCenter.default.addObserver(self, + /*NotificationCenter.default.addObserver(self, selector: #selector(reloadViewModel(_:)), - // swiftlint:disable:next line_length - name: Notification.Name(rawValue: Constants.completedFirstSyncAfterLogin), - object: nil) + name: Notification.Name(rawValue: Constants.shouldRefreshView), + object: nil)*/ } // MARK: Helpers (private) @@ -51,6 +120,12 @@ class ProfileViewModel: ObservableObject { } clearSubscriptions() + do { + try await fetchProfilePicture() + } catch { + Logger.profile.error("Could not fetch profile image: \(error)") + } + // Build query to search for OCKPatient // swiftlint:disable:next line_length var queryForCurrentPatient = OCKPatientQuery(for: Date()) // This makes the query for the current version of Patient @@ -59,12 +134,23 @@ class ProfileViewModel: ObservableObject { do { guard let appDelegate = AppDelegateKey.defaultValue, let foundPatient = try await appDelegate.store?.fetchPatients(query: queryForCurrentPatient), - let currentPatient = foundPatient.first else { + let currentPatient = foundPatient.first else { // swiftlint:disable:next line_length Logger.profile.error("Could not find patient with id \"\(uuid)\". It's possible they have never been saved.") return } self.observePatient(currentPatient) + + // Query the contact also so the user can edit + var queryForCurrentContact = OCKContactQuery(for: Date()) + queryForCurrentContact.ids = [uuid.uuidString] + guard let foundContact = try await appDelegate.store?.fetchContacts(query: queryForCurrentContact), + let currentContact = foundContact.first else { + // swiftlint:disable:next line_length + Logger.profile.error("Error: Could not find contact with id \"\(uuid)\". It's possible they have never been saved.") + return + } + self.observeContact(currentContact) } catch { // swiftlint:disable:next line_length Logger.profile.error("Could not find patient with id \"\(uuid)\". It's possible they have never been saved. Query error: \(error.localizedDescription)") @@ -81,33 +167,144 @@ class ProfileViewModel: ObservableObject { .store(in: &cancellables) } - // MARK: User intentional behavior @MainActor - func saveProfile(_ first: String, last: String, birth: Date) async throws { + private func findAndObserveCurrentContact() async { + guard let uuid = try? Utility.getRemoteClockUUID() else { + Logger.profile.error("Could not get remote uuid for this user.") + return + } + clearSubscriptions() + + // Build query to search for OCKPatient + // swiftlint:disable:next line_length + var queryForCurrentPatient = OCKPatientQuery(for: Date()) // This makes the query for the current version of Patient + queryForCurrentPatient.ids = [uuid.uuidString] // Search for the current logged in user + + do { + guard let appDelegate = AppDelegateKey.defaultValue, + let foundPatient = try await appDelegate.store?.fetchPatients(query: queryForCurrentPatient), + let currentPatient = foundPatient.first else { + // swiftlint:disable:next line_length + Logger.profile.error("Could not find patient with id \"\(uuid)\". It's possible they have never been saved.") + return + } + self.observePatient(currentPatient) + + // Query the contact also so the user can edit + var queryForCurrentContact = OCKContactQuery(for: Date()) + queryForCurrentContact.ids = [uuid.uuidString] + guard let foundContact = try await appDelegate.store?.fetchContacts(query: queryForCurrentContact), + let currentContact = foundContact.first else { + // swiftlint:disable:next line_length + Logger.profile.error("Error: Could not find contact with id \"\(uuid)\". It's possible they have never been saved.") + return + } + self.observeContact(currentContact) + + try? await fetchProfilePicture() + } catch { + // swiftlint:disable:next line_length + Logger.profile.error("Could not find patient with id \"\(uuid)\". It's possible they have never been saved. Query error: \(error.localizedDescription)") + } + } + + @MainActor + private func observeContact(_ contact: OCKContact) { + + storeManager.publisher(forContact: contact, + categories: [.add, .update, .delete]) + .sink { [weak self] in + self?.contact = $0 as? OCKContact + } + .store(in: &cancellables) + } + + @MainActor + private func fetchProfilePicture() async throws { + // Profile pics are stored in Parse User. + guard let currentUser = try await User.current?.fetch() else { + Logger.profile.error("User is not logged in") + return + } + + if let pictureFile = currentUser.profilePicture { + + // Download picture from server if needed + do { + let profilePicture = try await pictureFile.fetch() + guard let path = profilePicture.localURL?.relativePath else { + Logger.profile.error("Could not find relative path for profile picture.") + return + } + self.profileUIImage = UIImage(contentsOfFile: path) + } catch { + Logger.profile.error("Could not fetch profile picture: \(error.localizedDescription).") + } + } + self.isSettingProfilePictureForFirstTime = false + } +} + +// MARK: User intentional behavior +extension ProfileViewModel { + @MainActor + func saveProfile() async { + alertMessage = "All changs saved successfully!" + do { + try await savePatient() + try await saveContact() + } catch { + alertMessage = "Could not save profile: \(error)" + } + isShowingSaveAlert = true // Make alert pop up for user. + } + + @MainActor + // swiftlint:disable cyclomatic_complexity + func savePatient() async throws { if var patientToUpdate = patient { // If there is a currentPatient that was fetched, check to see if any of the fields changed var patientHasBeenUpdated = false - if patient?.name.givenName != first { + if patient?.name.givenName != firstName { patientHasBeenUpdated = true - patientToUpdate.name.givenName = first + patientToUpdate.name.givenName = firstName } - if patient?.name.familyName != last { + if patient?.name.familyName != lastName { patientHasBeenUpdated = true - patientToUpdate.name.familyName = last + patientToUpdate.name.familyName = lastName } - if patient?.birthday != birth { + if patient?.birthday != birthday { patientHasBeenUpdated = true - patientToUpdate.birthday = birth + patientToUpdate.birthday = birthday + } + + if patient?.sex != sex { + patientHasBeenUpdated = true + patientToUpdate.sex = sex + } + + if patient?.allergies != [allergies] { + patientHasBeenUpdated = true + patientToUpdate.allergies = [allergies] + } + + let notes = [OCKNote(author: firstName, + title: "New Note", + content: note)] + if patient?.notes != notes { + patientHasBeenUpdated = true + patientToUpdate.notes = notes } if patientHasBeenUpdated { let updated = try await storeManager.store.updateAnyPatient(patientToUpdate) Logger.profile.info("Successfully updated patient") guard let updatedPatient = updated as? OCKPatient else { + Logger.profile.error("Could not cast to OCKPatient") return } self.patient = updatedPatient @@ -119,8 +316,10 @@ class ProfileViewModel: ObservableObject { return } - var newPatient = OCKPatient(id: remoteUUID, givenName: first, familyName: last) - newPatient.birthday = birth + var newPatient = OCKPatient(id: remoteUUID, + givenName: firstName, + familyName: lastName) + newPatient.birthday = birthday // This is new patient that has never been saved before let addedPatient = try await storeManager.store.addAnyPatient(newPatient) @@ -130,6 +329,95 @@ class ProfileViewModel: ObservableObject { return } self.patient = addedOCKPatient + self.observePatient(addedOCKPatient) + } + } + + @MainActor + func saveContact() async throws { + + if var contactToUpdate = contact { + // If a current contact was fetched, check to see if any of the fields have changed + + var contactHasBeenUpdated = false + + // Since OCKPatient was updated earlier, we should compare against this name + if let patientName = patient?.name, + contact?.name != patient?.name { + contactHasBeenUpdated = true + contactToUpdate.name = patientName + } + + // Create a mutable temp address to compare + let potentialAddress = OCKPostalAddress() + potentialAddress.street = street + potentialAddress.city = city + potentialAddress.state = state + potentialAddress.postalCode = zipcode + + if contact?.address != potentialAddress { + contactHasBeenUpdated = true + contactToUpdate.address = potentialAddress + } + + let tempEmail = OCKLabeledValue(label: "email", value: email) + let tempMessagingNumber = OCKLabeledValue(label: "messaging number", value: messagingNumber) + let tempPhoneNumber = OCKLabeledValue(label: "phone number", value: phoneNumber) + let tempOtherContactInfo = OCKLabeledValue(label: "other contact info", value: otherContactInfo) + + if contact?.emailAddresses != [tempEmail] { + contactHasBeenUpdated = true + contactToUpdate.emailAddresses = [tempEmail] + } + + if contact?.messagingNumbers != [tempMessagingNumber] { + contactHasBeenUpdated = true + contactToUpdate.messagingNumbers = [tempMessagingNumber] + } + + if contact?.phoneNumbers != [tempPhoneNumber] { + contactHasBeenUpdated = true + contactToUpdate.phoneNumbers = [tempPhoneNumber] + } + + if contact?.otherContactInfo != [tempOtherContactInfo] { + contactHasBeenUpdated = true + contactToUpdate.otherContactInfo = [tempOtherContactInfo] + } + + if contactHasBeenUpdated { + let updated = try await storeManager.store.updateAnyContact(contactToUpdate) + Logger.profile.info("Successfully updated contact") + guard let updatedContact = updated as? OCKContact else { + Logger.profile.error("Could not cast to OCKContact") + return + } + self.contact = updatedContact + } + + } else { + + guard let remoteUUID = try? Utility.getRemoteClockUUID().uuidString else { + Logger.profile.error("The user currently is not logged in") + return + } + + guard let patientName = self.patient?.name else { + Logger.profile.info("The patient did not have a name.") + return + } + + // Added code to create a contact for the respective signed up user + let newContact = OCKContact(id: remoteUUID, + name: patientName, + carePlanUUID: nil) + + guard let addedContact = try await storeManager.store.addAnyContact(newContact) as? OCKContact else { + Logger.profile.error("Could not cast to OCKContact") + return + } + self.contact = addedContact + self.observeContact(addedContact) } } } diff --git a/OCKSample/Main/Profile/TaskView.swift b/OCKSample/Main/Profile/TaskView.swift new file mode 100755 index 0000000..c6757ff --- /dev/null +++ b/OCKSample/Main/Profile/TaskView.swift @@ -0,0 +1,56 @@ +// +// TaskView.swift +// OCKSample +// +// Created by on 10/27/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import SwiftUI +import CareKitUI + +struct TaskView: View { + @StateObject var viewModel = TaskViewModel() + @State var title = "" + @State var instructions = "" + @State var schedule = Date() + + var body: some View { + NavigationView { + VStack { + VStack(alignment: .leading) { + TextField("Title", text: $title) + .padding() + .cornerRadius(20.0) + .shadow(radius: 10.0, x: 20, y: 10) + + TextField("Instructions", text: $instructions) + .padding() + .cornerRadius(20.0) + .shadow(radius: 10.0, x: 20, y: 10) + + DatePicker("Schedule", selection: $schedule, displayedComponents: [DatePickerComponents.date]) + .padding() + .cornerRadius(20.0) + .shadow(radius: 10.0, x: 20, y: 10) + } + + Button(action: { + Task { + do { + await viewModel.addTask(title, instructions: instructions, schedule: schedule) + } + } + }, label: { + Text("Save Task") + .font(.headline) + .foregroundColor(.white) + .padding() + .frame(width: 300, height: 50) + }) + .background(Color(.green)) + .cornerRadius(15) + } + } + } +} diff --git a/OCKSample/Main/Profile/TaskViewModel.swift b/OCKSample/Main/Profile/TaskViewModel.swift new file mode 100755 index 0000000..fec5e5b --- /dev/null +++ b/OCKSample/Main/Profile/TaskViewModel.swift @@ -0,0 +1,50 @@ +// +// TaskViewModel.swift +// OCKSample +// +// Created by on 10/27/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation +import CareKit +import CareKitStore +import SwiftUI + +class TaskViewModel: ObservableObject { + + @Published var task = OCKTask(id: "", + title: nil, + carePlanUUID: nil, + schedule: .dailyAtTime(hour: 0, + minutes: 0, + start: Date(), + end: nil, + text: nil)) + @Published var error: AppError? + + // MARK: Intents + func addTask(_ title: String, instructions: String, schedule: Date) async { + guard let appDelegate = AppDelegateKey.defaultValue else { + error = AppError.couldntBeUnwrapped + return + } + + var updateTask = OCKTask(id: title, + title: title, + carePlanUUID: nil, + schedule: .dailyAtTime(hour: 0, + minutes: 0, + start: Date(), + end: nil, + text: nil)) + + updateTask.instructions = instructions + + do { + try await appDelegate.store?.addTasksIfNotPresent([updateTask]) + } catch { + self.error = AppError.errorString("Couldn't add task: \(error.localizedDescription)") + } + } +} diff --git a/OCKSample/Main/Stylers/AnimationStyle.swift b/OCKSample/Main/Stylers/AnimationStyle.swift new file mode 100755 index 0000000..36993a0 --- /dev/null +++ b/OCKSample/Main/Stylers/AnimationStyle.swift @@ -0,0 +1,14 @@ +// +// AnimationStyle.swift +// OCKSample +// +// Created by on 10/14/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation +import CareKitUI + +struct AnimationStyle: OCKAnimationStyler { + var stateChangeDuration: Double { 2 } +} diff --git a/OCKSample/Main/Stylers/AppearanceStyle.swift b/OCKSample/Main/Stylers/AppearanceStyle.swift new file mode 100755 index 0000000..f3add0a --- /dev/null +++ b/OCKSample/Main/Stylers/AppearanceStyle.swift @@ -0,0 +1,18 @@ +// +// AppearanceStyle().swift +// OCKSample +// +// Created by on 10/14/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation +import CareKitUI + +struct AppearanceStyle: OCKAppearanceStyler { + var cornerRadius1: CGFloat { 25 } + var cornerRadius2: CGFloat { 20 } + + var borderWidth1: CGFloat { 15 } + var borderWidth2: CGFloat { 8 } +} diff --git a/OCKSample/Main/Stylers/ColorStyler.swift b/OCKSample/Main/Stylers/ColorStyler.swift old mode 100644 new mode 100755 index e539fcd..e50f79a --- a/OCKSample/Main/Stylers/ColorStyler.swift +++ b/OCKSample/Main/Stylers/ColorStyler.swift @@ -11,6 +11,14 @@ import CareKitUI import UIKit struct ColorStyler: OCKColorStyler { + + var customBackground: UIColor { #colorLiteral(red: 0.4745098054, green: 0.8392156959, blue: 0.9764705896, alpha: 1) } + var secondaryCustomBackground: UIColor { #colorLiteral(red: 0.721568644, green: 0.8862745166, blue: 0.5921568871, alpha: 1) } + + var customGroupedBackground: UIColor { #colorLiteral(red: 0.8313068151, green: 0.6976719499, blue: 0.9890167117, alpha: 1) } + var secondaryCustomGroupedBackground: UIColor { #colorLiteral(red: 1, green: 0.7050126195, blue: 0.8364293575, alpha: 1) } + var tertiaryCustomGroupedBackground: UIColor { #colorLiteral(red: 0.9764705896, green: 0.850980401, blue: 0.5490196347, alpha: 1) } + #if os(iOS) var label: UIColor { FontColorKey.defaultValue diff --git a/OCKSample/Main/Stylers/DimensionStyle.swift b/OCKSample/Main/Stylers/DimensionStyle.swift new file mode 100755 index 0000000..be2e9f0 --- /dev/null +++ b/OCKSample/Main/Stylers/DimensionStyle.swift @@ -0,0 +1,18 @@ +// +// DimensionStyle.swift +// OCKSample +// +// Created by on 10/14/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation +import CareKitUI + +struct DimensionStyle: OCKDimensionStyler { + var imageHeight2: CGFloat { 10 } + var imageHeight1: CGFloat { 10 } + var pointSize3: CGFloat { 20 } + var pointSize2: CGFloat { 30 } + var pointSize1: CGFloat { 40 } +} diff --git a/OCKSample/Main/Stylers/Styler.swift b/OCKSample/Main/Stylers/Styler.swift old mode 100644 new mode 100755 index 049f58d..52eb2bd --- a/OCKSample/Main/Stylers/Styler.swift +++ b/OCKSample/Main/Stylers/Styler.swift @@ -14,12 +14,12 @@ struct Styler: OCKStyler { ColorStyler() } var dimension: OCKDimensionStyler { - OCKDimensionStyle() + DimensionStyle() } var animation: OCKAnimationStyler { - OCKAnimationStyle() + AnimationStyle() } var appearance: OCKAppearanceStyler { - OCKAppearanceStyle() + AppearanceStyle() } } diff --git a/OCKSample/Main/Surveys/CheckIn.swift b/OCKSample/Main/Surveys/CheckIn.swift new file mode 100755 index 0000000..bdaa45a --- /dev/null +++ b/OCKSample/Main/Surveys/CheckIn.swift @@ -0,0 +1,116 @@ +// +// CheckIn.swift +// OCKSample +// +// Created by Corey Baker on 11/11/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import CareKitStore +#if canImport(ResearchKit) +import ResearchKit +#endif + +struct CheckIn: Surveyable { + static var surveyType: Survey { + Survey.checkIn + } + + static var formIdentifier: String { + "\(Self.identifier()).form" + } + + static var painItemIdentifier: String { + "\(Self.identifier()).form.pain" + } + + static var sleepItemIdentifier: String { + "\(Self.identifier()).form.sleep" + } +} + +#if canImport(ResearchKit) +extension CheckIn { + func createSurvey() -> ORKTask { + + let painAnswerFormat = ORKAnswerFormat.scale( + withMaximumValue: 12, + minimumValue: 0, + defaultValue: 0, + step: 1, + vertical: false, + maximumValueDescription: "12 minute", + minimumValueDescription: "1 minutes" + ) + + let painItem = ORKFormItem( + identifier: Self.painItemIdentifier, + text: "How long was your last shower?", + answerFormat: painAnswerFormat + ) + painItem.isOptional = false + + let sleepAnswerFormat = ORKAnswerFormat.scale( + withMaximumValue: 12, + minimumValue: 0, + defaultValue: 0, + step: 1, + vertical: false, + maximumValueDescription: nil, + minimumValueDescription: nil + ) + + let sleepItem = ORKFormItem( + identifier: Self.sleepItemIdentifier, + text: "Estiamte how many fluid ounces of shampoo you used.", + answerFormat: sleepAnswerFormat + ) + sleepItem.isOptional = false + + let formStep = ORKFormStep( + identifier: Self.formIdentifier, + title: "Check In", + text: "Please answer the following questions." + ) + formStep.formItems = [painItem, sleepItem] + formStep.isOptional = false + + let surveyTask = ORKOrderedTask( + identifier: identifier(), + steps: [formStep] + ) + return surveyTask + } + + func extractAnswers(_ result: ORKTaskResult) -> [OCKOutcomeValue]? { + + guard + let response = result.results? + .compactMap({ $0 as? ORKStepResult }) + .first(where: { $0.identifier == Self.formIdentifier }), + + let scaleResults = response + .results?.compactMap({ $0 as? ORKScaleQuestionResult }), + + let painAnswer = scaleResults + .first(where: { $0.identifier == Self.painItemIdentifier })? + .scaleAnswer, + + let sleepAnswer = scaleResults + .first(where: { $0.identifier == Self.sleepItemIdentifier })? + .scaleAnswer + else { + assertionFailure("Failed to extract answers from check in survey!") + return nil + } + + var painValue = OCKOutcomeValue(Double(truncating: painAnswer)) + painValue.kind = Self.painItemIdentifier + + var sleepValue = OCKOutcomeValue(Double(truncating: sleepAnswer)) + sleepValue.kind = Self.sleepItemIdentifier + + return [painValue, sleepValue] + } +} +#endif diff --git a/OCKSample/Main/Surveys/Onboard.swift b/OCKSample/Main/Surveys/Onboard.swift new file mode 100755 index 0000000..d3b23a9 --- /dev/null +++ b/OCKSample/Main/Surveys/Onboard.swift @@ -0,0 +1,164 @@ +// +// Onboarding.swift +// OCKSample +// +// Created by Corey Baker on 11/11/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation +import CareKitStore +#if canImport(ResearchKit) +import ResearchKit +#endif + +struct Onboard: Surveyable { + static var surveyType: Survey { + Survey.onboard + } +} + +#if canImport(ResearchKit) +extension Onboard { + /* + xTODO: Modify the onboarding so it properly represents the + usecase of your application. Changes should be made to + each of the steps in this type method. For example, you + should change: title, detailText, image, and imageContentMode, + and learnMoreItem. + */ + func createSurvey() -> ORKTask { + // The Welcome Instruction step. + let welcomeInstructionStep = ORKInstructionStep( + identifier: "\(identifier()).welcome" + ) + + welcomeInstructionStep.title = "Hello!" + + welcomeInstructionStep.detailText = "To use the app you need to join our study!" + welcomeInstructionStep.image = UIImage(named: "star") + welcomeInstructionStep.imageContentMode = .scaleAspectFill + + // The Informed Consent Instruction step. + let studyOverviewInstructionStep = ORKInstructionStep( + identifier: "\(identifier()).overview" + ) + + studyOverviewInstructionStep.title = "Things you need to know" + studyOverviewInstructionStep.iconImage = UIImage(systemName: "checkmark.seal.fill") + + let heartBodyItem = ORKBodyItem( + text: "The study will ask you general health questions.", + detailText: nil, + image: UIImage(systemName: "heart.fill"), + learnMoreItem: nil, + bodyItemStyle: .image + ) + + let completeTasksBodyItem = ORKBodyItem( + text: "You will be asked to provide reliable shower data.", + detailText: nil, + image: UIImage(systemName: "checkmark.circle.fill"), + learnMoreItem: nil, + bodyItemStyle: .image + ) + + let signatureBodyItem = ORKBodyItem( + text: "You will sign your name on an informed consent document", + detailText: nil, + image: UIImage(systemName: "signature"), + learnMoreItem: nil, + bodyItemStyle: .image + ) + + let secureDataBodyItem = ORKBodyItem( + text: "Your data will be secure.", + detailText: nil, + image: UIImage(systemName: "lock.fill"), + learnMoreItem: nil, + bodyItemStyle: .image + ) + + studyOverviewInstructionStep.bodyItems = [ + heartBodyItem, + completeTasksBodyItem, + signatureBodyItem, + secureDataBodyItem + ] + + // The Signature step (using WebView). + let webViewStep = ORKWebViewStep( + identifier: "\(identifier()).signatureCapture", + html: informedConsentHTML + ) + + webViewStep.showSignatureAfterContent = true + + // The Request Permissions step. + // xTODO: Set these to HealthKit info you want to display + // by default. + let healthKitTypesToWrite: Set = [ + .quantityType(forIdentifier: .bloodAlcoholContent)!, + .quantityType(forIdentifier: .waterTemperature)!, + .workoutType() + ] + + let healthKitTypesToRead: Set = [ + .characteristicType(forIdentifier: .dateOfBirth)!, + .workoutType(), + .quantityType(forIdentifier: .bloodAlcoholContent)!, + .quantityType(forIdentifier: .waterTemperature)! + ] + + let healthKitPermissionType = ORKHealthKitPermissionType( + sampleTypesToWrite: healthKitTypesToWrite, + objectTypesToRead: healthKitTypesToRead + ) + + let notificationsPermissionType = ORKNotificationPermissionType( + authorizationOptions: [.alert, .badge, .criticalAlert, .sound] + ) + + let motionPermissionType = ORKMotionActivityPermissionType() + + let requestPermissionsStep = ORKRequestPermissionsStep( + identifier: "\(identifier()).requestPermissionsStep", + permissionTypes: [ + healthKitPermissionType, + notificationsPermissionType, + motionPermissionType + ] + ) + + requestPermissionsStep.title = "Data Request" + requestPermissionsStep.text = "You must allow the following for the study." + + // Completion Step + let completionStep = ORKCompletionStep( + identifier: "\(identifier()).completionStep" + ) + + completionStep.title = "Enrollment Complete" + completionStep.text = "Thank you for participating in our study! You can opt out any time." + + let surveyTask = ORKOrderedTask( + identifier: identifier(), + steps: [ + welcomeInstructionStep, + studyOverviewInstructionStep, + webViewStep, + requestPermissionsStep, + completionStep + ] + ) + return surveyTask + } + + func extractAnswers(_ result: ORKTaskResult) -> [CareKitStore.OCKOutcomeValue]? { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + Utility.requestHealthKitPermissions() + } + return [OCKOutcomeValue(Date())] + } +} +#endif diff --git a/OCKSample/Main/Surveys/RangeOfMotion.swift b/OCKSample/Main/Surveys/RangeOfMotion.swift new file mode 100755 index 0000000..bcbdcec --- /dev/null +++ b/OCKSample/Main/Surveys/RangeOfMotion.swift @@ -0,0 +1,59 @@ +// +// RangeOfMotion.swift +// OCKSample +// +// Created by Corey Baker on 11/11/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import CareKitStore +#if canImport(ResearchKit) +import ResearchKit +#endif + +struct RangeOfMotion: Surveyable { + static var surveyType: Survey { + Survey.rangeOfMotion + } +} + +#if canImport(ResearchKit) +extension RangeOfMotion { + func createSurvey() -> ORKTask { + + let rangeOfMotionOrderedTask = ORKOrderedTask.shoulderRangeOfMotionTask( + withIdentifier: identifier(), + limbOption: .left, + intendedUseDescription: nil, + options: [.excludeConclusion] + ) + + let completionStep = ORKCompletionStep(identifier: "\(identifier()).completion") + completionStep.title = "All done!" + completionStep.detailText = "Keep that shoulder loosened up!" + + rangeOfMotionOrderedTask.addSteps(from: [completionStep]) + + return rangeOfMotionOrderedTask + } + + func extractAnswers(_ result: ORKTaskResult) -> [OCKOutcomeValue]? { + guard let motionResult = result.results? + .compactMap({ $0 as? ORKStepResult }) + .compactMap({ $0.results }) + .flatMap({ $0 }) + .compactMap({ $0 as? ORKRangeOfMotionResult }) + .first else { + + assertionFailure("Failed to parse range of motion result") + return nil + } + + var range = OCKOutcomeValue(motionResult.range) + + range.kind = #keyPath(ORKRangeOfMotionResult.range) + + return [range] + } +} +#endif diff --git a/OCKSample/Main/Surveys/Survey.swift b/OCKSample/Main/Surveys/Survey.swift new file mode 100755 index 0000000..a99815f --- /dev/null +++ b/OCKSample/Main/Surveys/Survey.swift @@ -0,0 +1,31 @@ +// +// Survey.swift +// OCKSample +// +// Created by Corey Baker on 11/11/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation +import CareKitStore +#if canImport(ResearchKit) +import ResearchKit +#endif + +enum Survey: String, CaseIterable, Identifiable { + var id: Self { self } + case onboard = "Onboard" + case checkIn = "Check In" + case rangeOfMotion = "Range of Motion" + + func type() -> Surveyable { + switch self { + case .onboard: + return Onboard() + case .checkIn: + return CheckIn() + case .rangeOfMotion: + return RangeOfMotion() + } + } +} diff --git a/OCKSample/Main/Surveys/Surveyable.swift b/OCKSample/Main/Surveys/Surveyable.swift new file mode 100755 index 0000000..1475bcd --- /dev/null +++ b/OCKSample/Main/Surveys/Surveyable.swift @@ -0,0 +1,40 @@ +// +// Surveyable.swift +// OCKSample +// +// Created by Corey Baker on 11/14/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation +import CareKitStore +#if canImport(ResearchKit) +import ResearchKit +#endif + +/** + Correlates a CareKit task with a ResearchKit task. + */ +protocol Surveyable { + /// The type of survey. + static var surveyType: Survey { get } + /// The unique identifier of the survey. + static func identifier() -> String + #if canImport(ResearchKit) + /// Creates the survey. + func createSurvey() -> ORKTask + /// Extracts the answers from the survey. + func extractAnswers(_ result: ORKTaskResult) -> [OCKOutcomeValue]? + #endif +} + +extension Surveyable { + static func identifier() -> String { + surveyType.rawValue.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + } + + /// The unique identifier of the survey. + func identifier() -> String { + Self.identifier() + } +} diff --git a/OCKSample/Models/Installation.swift b/OCKSample/Models/Installation.swift old mode 100644 new mode 100755 diff --git a/OCKSample/Models/ScheduleUtility.swift b/OCKSample/Models/ScheduleUtility.swift new file mode 100644 index 0000000..efd304a --- /dev/null +++ b/OCKSample/Models/ScheduleUtility.swift @@ -0,0 +1,117 @@ +// +// ScheduleUtility.swift +// OCKSample +// +// Created by Corey Baker on 12/3/22. +// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. +// + +import CareKitStore +import CareKitUI +import Foundation + +struct ScheduleUtility { + + private static let timeFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.timeStyle = .short + return formatter + }() + + private static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .short + return formatter + }() + + static func scheduleLabel(for events: [OCKAnyEvent]) -> String? { + let result = [completionLabel(for: events), dateLabel(for: events)] + .compactMap { $0 } + .joined(separator: " ") + return !result.isEmpty ? result : nil + } + + static func scheduleLabel(for event: OCKAnyEvent?) -> String? { + guard let event = event else { return nil } + let result = [ + timeLabel(for: event), + dateLabel(forStart: event.scheduleEvent.start, end: event.scheduleEvent.end) + ] + .compactMap { $0 } + .joined(separator: " ") + + return !result.isEmpty ? result : nil + } + + static func timeLabel(for event: OCKAnyEvent, includesEnd: Bool = true) -> String { + if let customText = event.scheduleEvent.element.text { + return customText + } + + switch event.scheduleEvent.element.duration { + + case .allDay: return "Anytime" + case .seconds: + if includesEnd && event.scheduleEvent.start != event.scheduleEvent.end { + let start = event.scheduleEvent.start + let end = event.scheduleEvent.end + return "\(timeFormatter.string(from: start)) " + loc("TO") + " \(timeFormatter.string(from: end))" + } + } + let label = timeFormatter.string(from: event.scheduleEvent.start).description + return label + } + + static func completedTimeLabel(for event: OCKAnyEvent) -> String? { + guard let completedDate = event.outcome?.values + .max(by: { isMoreRecent(lhs: $0.createdDate, rhs: $1.createdDate) })? + .createdDate + else { return nil } + return timeFormatter.string(from: completedDate) + } + + private static func dateLabel(for events: [OCKAnyEvent]) -> String? { + guard !events.isEmpty else { return nil } + if events.count > 1 { + let schedule = events.first!.scheduleEvent + return dateLabel(forStart: schedule.start, end: schedule.end) + } + return dateLabel(forStart: events.first!.scheduleEvent.start, end: events.last!.scheduleEvent.end) + } + + private static func isMoreRecent(lhs: Date?, rhs: Date?) -> Bool { + guard let lhs = lhs else { return false } + guard let rhs = rhs else { return true } + return lhs > rhs + } + + private static func dateLabel(forStart start: Date, end: Date) -> String? { + let datesAreInSameDay = Calendar.current.isDate(start, inSameDayAs: end) + if datesAreInSameDay { + let datesAreToday = Calendar.current.isDateInToday(start) + return !datesAreToday ? loc("ON") + " " + "\(label(for: start))" : nil + } + return loc("FROM") + " \(label(for: start))" + loc("TO") + " \(label(for: end))" + } + + private static func label(for date: Date) -> String { + if Calendar.current.isDateInToday(date) { + return loc("TODAY") + } + let label = dateFormatter.string(from: date) + return label + } + + private static func completionLabel(for events: [OCKAnyEvent]) -> String? { + guard !events.isEmpty else { return nil } + let completed = events.filter { $0.outcome != nil }.count + let remaining = events.count - completed + let format = OCKLocalization.localized("EVENTS_REMAINING", + tableName: "Localizable", + bundle: nil, + value: "", + // swiftlint:disable:next line_length + comment: "The number of events that the user has not yet marked completed") + return String.localizedStringWithFormat(format, remaining) + } +} diff --git a/OCKSample/Models/User.swift b/OCKSample/Models/User.swift old mode 100644 new mode 100755 index 27d6bec..6256ec6 --- a/OCKSample/Models/User.swift +++ b/OCKSample/Models/User.swift @@ -25,6 +25,7 @@ struct User: ParseUser { // Custom properties var lastTypeSelected: String? var userTypeUUIDs: [String: UUID]? + var profilePicture: ParseFile? } // MARK: Default Implementation @@ -39,6 +40,10 @@ extension User { original: object) { updated.userTypeUUIDs = object.userTypeUUIDs } + if updated.shouldRestoreKey(\.profilePicture, + original: object) { + updated.profilePicture = object.profilePicture + } return updated } } diff --git a/OCKSample/OCKSample.entitlements b/OCKSample/OCKSample.entitlements old mode 100644 new mode 100755 diff --git a/OCKSample/OCKSampleApp.swift b/OCKSample/OCKSampleApp.swift old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json old mode 100644 new mode 100755 index e045665..f9813a2 --- a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,196 +1,238 @@ { "images" : [ { - "size" : "20x20", - "idiom" : "iphone", "filename" : "Icon-App-20x20@2x-1.png", - "scale" : "2x" + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" }, { - "size" : "20x20", - "idiom" : "iphone", "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" }, { - "size" : "29x29", - "idiom" : "iphone", "filename" : "Icon-App-29x29@2x-2.png", - "scale" : "2x" + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" }, { - "size" : "29x29", - "idiom" : "iphone", "filename" : "Icon-Homescreen-29x29@3x.png", - "scale" : "3x" + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" }, { - "size" : "40x40", - "idiom" : "iphone", "filename" : "Icon-App-40x40@2x-2.png", - "scale" : "2x" + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" }, { - "size" : "40x40", - "idiom" : "iphone", "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" }, { - "size" : "60x60", - "idiom" : "iphone", "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" }, { - "size" : "60x60", - "idiom" : "iphone", "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" }, { - "size" : "20x20", - "idiom" : "ipad", "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" }, { - "size" : "20x20", - "idiom" : "ipad", "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" }, { - "size" : "29x29", - "idiom" : "ipad", "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" }, { - "size" : "29x29", - "idiom" : "ipad", "filename" : "Icon-App-29x29@2x-1.png", - "scale" : "2x" + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" }, { - "size" : "40x40", - "idiom" : "ipad", "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" }, { - "size" : "40x40", - "idiom" : "ipad", "filename" : "Icon-App-40x40@2x-1.png", - "scale" : "2x" + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" }, { - "size" : "76x76", - "idiom" : "ipad", "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" }, { - "size" : "76x76", - "idiom" : "ipad", "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" }, { - "size" : "83.5x83.5", - "idiom" : "ipad", "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" }, { - "size" : "1024x1024", - "idiom" : "ios-marketing", "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" }, { - "size" : "24x24", - "idiom" : "watch", "filename" : "Icon-Homescreen-24x24@2x.png", - "scale" : "2x", + "idiom" : "watch", "role" : "notificationCenter", + "scale" : "2x", + "size" : "24x24", "subtype" : "38mm" }, { - "size" : "27.5x27.5", - "idiom" : "watch", "filename" : "Icon-Homescreen-27.5x27.5@2x.png", - "scale" : "2x", + "idiom" : "watch", "role" : "notificationCenter", + "scale" : "2x", + "size" : "27.5x27.5", "subtype" : "42mm" }, { - "size" : "29x29", - "idiom" : "watch", "filename" : "Icon-Homescreen-29x29@2x.png", + "idiom" : "watch", "role" : "companionSettings", - "scale" : "2x" + "scale" : "2x", + "size" : "29x29" }, { - "size" : "29x29", - "idiom" : "watch", "filename" : "Icon-Homescreen-29x29@3x-1.png", + "idiom" : "watch", "role" : "companionSettings", - "scale" : "3x" + "scale" : "3x", + "size" : "29x29" }, { - "size" : "40x40", "idiom" : "watch", - "filename" : "Icon-Homescreen-40x40@2x.png", + "role" : "notificationCenter", "scale" : "2x", + "size" : "33x33", + "subtype" : "45mm" + }, + { + "filename" : "Icon-Homescreen-40x40@2x.png", + "idiom" : "watch", "role" : "appLauncher", + "scale" : "2x", + "size" : "40x40", "subtype" : "38mm" }, { - "size" : "44x44", "idiom" : "watch", - "scale" : "2x", "role" : "appLauncher", + "scale" : "2x", + "size" : "44x44", "subtype" : "40mm" }, { - "size" : "50x50", "idiom" : "watch", + "role" : "appLauncher", "scale" : "2x", + "size" : "46x46", + "subtype" : "41mm" + }, + { + "idiom" : "watch", "role" : "appLauncher", + "scale" : "2x", + "size" : "50x50", "subtype" : "44mm" }, { - "size" : "86x86", "idiom" : "watch", - "filename" : "Icon-Homescreen-86x86@2x.png", + "role" : "appLauncher", "scale" : "2x", + "size" : "51x51", + "subtype" : "45mm" + }, + { + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "54x54", + "subtype" : "49mm" + }, + { + "filename" : "Icon-Homescreen-86x86@2x.png", + "idiom" : "watch", "role" : "quickLook", + "scale" : "2x", + "size" : "86x86", "subtype" : "38mm" }, { - "size" : "98x98", - "idiom" : "watch", "filename" : "Icon-Homescreen-98x98@2x.png", - "scale" : "2x", + "idiom" : "watch", "role" : "quickLook", + "scale" : "2x", + "size" : "98x98", "subtype" : "42mm" }, { + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", "size" : "108x108", + "subtype" : "44mm" + }, + { "idiom" : "watch", + "role" : "quickLook", "scale" : "2x", + "size" : "117x117", + "subtype" : "45mm" + }, + { + "idiom" : "watch", "role" : "quickLook", - "subtype" : "44mm" + "scale" : "2x", + "size" : "129x129", + "subtype" : "49mm" }, { "idiom" : "watch-marketing", - "size" : "1024x1024", - "scale" : "1x" + "scale" : "1x", + "size" : "1024x1024" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-2.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-2.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-2.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-2.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-24x24@2x.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-24x24@2x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-27.5x27.5@2x.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-27.5x27.5@2x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-29x29@2x.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-29x29@2x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-29x29@3x-1.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-29x29@3x-1.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-29x29@3x.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-29x29@3x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-40x40@2x.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-40x40@2x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-86x86@2x.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-86x86@2x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-98x98@2x.png b/OCKSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-98x98@2x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/Contents.json b/OCKSample/Supporting Files/Assets.xcassets/Contents.json old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/carecard-filled.imageset/CareCard-OFF@1x.png b/OCKSample/Supporting Files/Assets.xcassets/carecard-filled.imageset/CareCard-OFF@1x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/carecard-filled.imageset/CareCard-OFF@2x.png b/OCKSample/Supporting Files/Assets.xcassets/carecard-filled.imageset/CareCard-OFF@2x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/carecard-filled.imageset/CareCard-OFF@3x.png b/OCKSample/Supporting Files/Assets.xcassets/carecard-filled.imageset/CareCard-OFF@3x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/carecard-filled.imageset/Contents.json b/OCKSample/Supporting Files/Assets.xcassets/carecard-filled.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/carecard.imageset/CareCard-ON@1x.png b/OCKSample/Supporting Files/Assets.xcassets/carecard.imageset/CareCard-ON@1x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/carecard.imageset/CareCard-ON@2x.png b/OCKSample/Supporting Files/Assets.xcassets/carecard.imageset/CareCard-ON@2x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/carecard.imageset/CareCard-ON@3x.png b/OCKSample/Supporting Files/Assets.xcassets/carecard.imageset/CareCard-ON@3x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/carecard.imageset/Contents.json b/OCKSample/Supporting Files/Assets.xcassets/carecard.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/connect-filled.imageset/Connect-ON@2x.png b/OCKSample/Supporting Files/Assets.xcassets/connect-filled.imageset/Connect-ON@2x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/connect-filled.imageset/Connect-ON@3x.png b/OCKSample/Supporting Files/Assets.xcassets/connect-filled.imageset/Connect-ON@3x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/connect-filled.imageset/Contents.json b/OCKSample/Supporting Files/Assets.xcassets/connect-filled.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/connect.imageset/Connect-OFF@2x.png b/OCKSample/Supporting Files/Assets.xcassets/connect.imageset/Connect-OFF@2x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/connect.imageset/Connect-OFF@3x.png b/OCKSample/Supporting Files/Assets.xcassets/connect.imageset/Connect-OFF@3x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/connect.imageset/Contents.json b/OCKSample/Supporting Files/Assets.xcassets/connect.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/exercise.jpg.imageset/Contents.json b/OCKSample/Supporting Files/Assets.xcassets/exercise.jpg.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/exercise.jpg.imageset/exercise-1.jpg b/OCKSample/Supporting Files/Assets.xcassets/exercise.jpg.imageset/exercise-1.jpg old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/exercise.jpg.imageset/exercise-2.jpg b/OCKSample/Supporting Files/Assets.xcassets/exercise.jpg.imageset/exercise-2.jpg old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/exercise.jpg.imageset/exercise.jpg b/OCKSample/Supporting Files/Assets.xcassets/exercise.jpg.imageset/exercise.jpg old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/insights-filled.imageset/Contents.json b/OCKSample/Supporting Files/Assets.xcassets/insights-filled.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/insights-filled.imageset/Insight-ON@1x.png b/OCKSample/Supporting Files/Assets.xcassets/insights-filled.imageset/Insight-ON@1x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/insights-filled.imageset/Insight-ON@2x.png b/OCKSample/Supporting Files/Assets.xcassets/insights-filled.imageset/Insight-ON@2x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/insights-filled.imageset/Insight-ON@3x.png b/OCKSample/Supporting Files/Assets.xcassets/insights-filled.imageset/Insight-ON@3x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/phone.bubble.left.fill.symbolset/Contents.json b/OCKSample/Supporting Files/Assets.xcassets/phone.bubble.left.fill.symbolset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/phone.bubble.left.fill.symbolset/phone.bubble.left.fill.svg b/OCKSample/Supporting Files/Assets.xcassets/phone.bubble.left.fill.symbolset/phone.bubble.left.fill.svg old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/phone.bubble.left.symbolset/Contents.json b/OCKSample/Supporting Files/Assets.xcassets/phone.bubble.left.symbolset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/phone.bubble.left.symbolset/phone.bubble.left.svg b/OCKSample/Supporting Files/Assets.xcassets/phone.bubble.left.symbolset/phone.bubble.left.svg old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/shower.imageset/Contents.json b/OCKSample/Supporting Files/Assets.xcassets/shower.imageset/Contents.json new file mode 100755 index 0000000..8bba7f0 --- /dev/null +++ b/OCKSample/Supporting Files/Assets.xcassets/shower.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "shower.jpeg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/OCKSample/Supporting Files/Assets.xcassets/shower.imageset/shower.jpeg b/OCKSample/Supporting Files/Assets.xcassets/shower.imageset/shower.jpeg new file mode 100755 index 0000000..3fcf4f6 Binary files /dev/null and b/OCKSample/Supporting Files/Assets.xcassets/shower.imageset/shower.jpeg differ diff --git a/OCKSample/Supporting Files/Assets.xcassets/showerTips.imageset/Contents.json b/OCKSample/Supporting Files/Assets.xcassets/showerTips.imageset/Contents.json new file mode 100644 index 0000000..ff0e8e3 --- /dev/null +++ b/OCKSample/Supporting Files/Assets.xcassets/showerTips.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "showerTips.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/OCKSample/Supporting Files/Assets.xcassets/showerTips.imageset/showerTips.png b/OCKSample/Supporting Files/Assets.xcassets/showerTips.imageset/showerTips.png new file mode 100644 index 0000000..a82be77 Binary files /dev/null and b/OCKSample/Supporting Files/Assets.xcassets/showerTips.imageset/showerTips.png differ diff --git a/OCKSample/Supporting Files/Assets.xcassets/symptoms-filled.imageset/Contents.json b/OCKSample/Supporting Files/Assets.xcassets/symptoms-filled.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/symptoms-filled.imageset/Symptom-ON@1x.png b/OCKSample/Supporting Files/Assets.xcassets/symptoms-filled.imageset/Symptom-ON@1x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/symptoms-filled.imageset/Symptom-ON@2x.png b/OCKSample/Supporting Files/Assets.xcassets/symptoms-filled.imageset/Symptom-ON@2x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/symptoms-filled.imageset/Symptom-ON@3x.png b/OCKSample/Supporting Files/Assets.xcassets/symptoms-filled.imageset/Symptom-ON@3x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/symptoms.imageset/Contents.json b/OCKSample/Supporting Files/Assets.xcassets/symptoms.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/symptoms.imageset/Symptom-OFF@1x.png b/OCKSample/Supporting Files/Assets.xcassets/symptoms.imageset/Symptom-OFF@1x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/symptoms.imageset/Symptom-OFF@2x.png b/OCKSample/Supporting Files/Assets.xcassets/symptoms.imageset/Symptom-OFF@2x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Assets.xcassets/symptoms.imageset/Symptom-OFF@3x.png b/OCKSample/Supporting Files/Assets.xcassets/symptoms.imageset/Symptom-OFF@3x.png old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Base.lproj/LaunchScreen.storyboard b/OCKSample/Supporting Files/Base.lproj/LaunchScreen.storyboard old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Info.plist b/OCKSample/Supporting Files/Info.plist old mode 100644 new mode 100755 index cd6df76..8c86ec7 --- a/OCKSample/Supporting Files/Info.plist +++ b/OCKSample/Supporting Files/Info.plist @@ -41,6 +41,8 @@ CareKit sample app would like to read from HealthKit NSHealthUpdateUsageDescription CareKit would like to write to your HealthKit data! + NSMotionUsageDescription + CareKit sample app would like to access your motion data UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/OCKSample/Supporting Files/Localization/OCKLocalization.swift b/OCKSample/Supporting Files/Localization/OCKLocalization.swift old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Localization/en.lproj/Localizable.strings b/OCKSample/Supporting Files/Localization/en.lproj/Localizable.strings old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/Localization/en.lproj/Localizable.stringsdict b/OCKSample/Supporting Files/Localization/en.lproj/Localizable.stringsdict old mode 100644 new mode 100755 diff --git a/OCKSample/Supporting Files/ParseCareKit.plist b/OCKSample/Supporting Files/ParseCareKit.plist old mode 100644 new mode 100755 index cfd4d06..d05292b --- a/OCKSample/Supporting Files/ParseCareKit.plist +++ b/OCKSample/Supporting Files/ParseCareKit.plist @@ -3,9 +3,9 @@ ApplicationID - E036A0C5-6829-4B40-9B3B-3E05F6DF32B2 + 82d161710b1665aa9e63b1403139d44cb7a6e13d30002ab879ada10a5429cc39 Server - http://localhost:1337/parse + https://amco-cs485-project.herokuapp.com/parse LiveQueryServer UseTransactions diff --git a/OCKSample/Utility.swift b/OCKSample/Utility.swift old mode 100644 new mode 100755 diff --git a/OCKSample/WatchConnectivity/LocalSyncSessionDelegate.swift b/OCKSample/WatchConnectivity/LocalSyncSessionDelegate.swift old mode 100644 new mode 100755 diff --git a/OCKSample/WatchConnectivity/RemoteSessionDelegate.swift b/OCKSample/WatchConnectivity/RemoteSessionDelegate.swift old mode 100644 new mode 100755 diff --git a/OCKSample/WatchConnectivity/SessionDelegate.swift b/OCKSample/WatchConnectivity/SessionDelegate.swift old mode 100644 new mode 100755 diff --git a/OCKSampleUITests/Info.plist b/OCKSampleUITests/Info.plist old mode 100644 new mode 100755 diff --git a/OCKSampleUITests/OCKSampleUITests.swift b/OCKSampleUITests/OCKSampleUITests.swift old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/AppDelegate.swift b/OCKWatchSample Extension/AppDelegate.swift old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json b/OCKWatchSample Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Assets.xcassets/Complication.complicationset/Contents.json b/OCKWatchSample Extension/Assets.xcassets/Complication.complicationset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json b/OCKWatchSample Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json b/OCKWatchSample Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json b/OCKWatchSample Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json b/OCKWatchSample Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json b/OCKWatchSample Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json b/OCKWatchSample Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json b/OCKWatchSample Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json b/OCKWatchSample Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Assets.xcassets/Contents.json b/OCKWatchSample Extension/Assets.xcassets/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/ContentView.swift b/OCKWatchSample Extension/ContentView.swift old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Extensions/AppDelegate+ParseRemoteDelegate.swift b/OCKWatchSample Extension/Extensions/AppDelegate+ParseRemoteDelegate.swift old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Info.plist b/OCKWatchSample Extension/Info.plist old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Main/Care/CareView.swift b/OCKWatchSample Extension/Main/Care/CareView.swift old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Main/Care/CareViewModel.swift b/OCKWatchSample Extension/Main/Care/CareViewModel.swift old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Main/Login/LoginView.swift b/OCKWatchSample Extension/Main/Login/LoginView.swift old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Main/Login/LoginViewModel.swift b/OCKWatchSample Extension/Main/Login/LoginViewModel.swift old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Main/MainView.swift b/OCKWatchSample Extension/Main/MainView.swift old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Notifications/NotificationController.swift b/OCKWatchSample Extension/Notifications/NotificationController.swift old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Notifications/NotificationView.swift b/OCKWatchSample Extension/Notifications/NotificationView.swift old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Notifications/PushNotificationPayload.apns b/OCKWatchSample Extension/Notifications/PushNotificationPayload.apns old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/OCKWatchSample Extension.entitlements b/OCKWatchSample Extension/OCKWatchSample Extension.entitlements old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/OCKWatchSampleApp.swift b/OCKWatchSample Extension/OCKWatchSampleApp.swift old mode 100644 new mode 100755 diff --git a/OCKWatchSample Extension/Preview Content/Preview Assets.xcassets/Contents.json b/OCKWatchSample Extension/Preview Content/Preview Assets.xcassets/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AccentColor.colorset/Contents.json b/OCKWatchSample/Assets.xcassets/AccentColor.colorset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Contents.json b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x-1.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x-1.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-2.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-2.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-2.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-2.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-24x24@2x.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-24x24@2x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-27.5x27.5@2x.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-27.5x27.5@2x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-29x29@2x.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-29x29@2x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-29x29@3x-1.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-29x29@3x-1.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-29x29@3x.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-29x29@3x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-40x40@2x.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-40x40@2x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-86x86@2x.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-86x86@2x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-98x98@2x.png b/OCKWatchSample/Assets.xcassets/AppIcon.appiconset/Icon-Homescreen-98x98@2x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/Contents.json b/OCKWatchSample/Assets.xcassets/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/carecard-filled.imageset/CareCard-OFF@1x.png b/OCKWatchSample/Assets.xcassets/carecard-filled.imageset/CareCard-OFF@1x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/carecard-filled.imageset/CareCard-OFF@2x.png b/OCKWatchSample/Assets.xcassets/carecard-filled.imageset/CareCard-OFF@2x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/carecard-filled.imageset/CareCard-OFF@3x.png b/OCKWatchSample/Assets.xcassets/carecard-filled.imageset/CareCard-OFF@3x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/carecard-filled.imageset/Contents.json b/OCKWatchSample/Assets.xcassets/carecard-filled.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/carecard.imageset/CareCard-ON@1x.png b/OCKWatchSample/Assets.xcassets/carecard.imageset/CareCard-ON@1x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/carecard.imageset/CareCard-ON@2x.png b/OCKWatchSample/Assets.xcassets/carecard.imageset/CareCard-ON@2x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/carecard.imageset/CareCard-ON@3x.png b/OCKWatchSample/Assets.xcassets/carecard.imageset/CareCard-ON@3x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/carecard.imageset/Contents.json b/OCKWatchSample/Assets.xcassets/carecard.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/connect-filled.imageset/Connect-ON@2x.png b/OCKWatchSample/Assets.xcassets/connect-filled.imageset/Connect-ON@2x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/connect-filled.imageset/Connect-ON@3x.png b/OCKWatchSample/Assets.xcassets/connect-filled.imageset/Connect-ON@3x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/connect-filled.imageset/Contents.json b/OCKWatchSample/Assets.xcassets/connect-filled.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/connect.imageset/Connect-OFF@2x.png b/OCKWatchSample/Assets.xcassets/connect.imageset/Connect-OFF@2x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/connect.imageset/Connect-OFF@3x.png b/OCKWatchSample/Assets.xcassets/connect.imageset/Connect-OFF@3x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/connect.imageset/Contents.json b/OCKWatchSample/Assets.xcassets/connect.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/insights-filled.imageset/Contents.json b/OCKWatchSample/Assets.xcassets/insights-filled.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/insights-filled.imageset/Insight-ON@1x.png b/OCKWatchSample/Assets.xcassets/insights-filled.imageset/Insight-ON@1x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/insights-filled.imageset/Insight-ON@2x.png b/OCKWatchSample/Assets.xcassets/insights-filled.imageset/Insight-ON@2x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/insights-filled.imageset/Insight-ON@3x.png b/OCKWatchSample/Assets.xcassets/insights-filled.imageset/Insight-ON@3x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/symptoms-filled.imageset/Contents.json b/OCKWatchSample/Assets.xcassets/symptoms-filled.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/symptoms-filled.imageset/Symptom-ON@1x.png b/OCKWatchSample/Assets.xcassets/symptoms-filled.imageset/Symptom-ON@1x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/symptoms-filled.imageset/Symptom-ON@2x.png b/OCKWatchSample/Assets.xcassets/symptoms-filled.imageset/Symptom-ON@2x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/symptoms-filled.imageset/Symptom-ON@3x.png b/OCKWatchSample/Assets.xcassets/symptoms-filled.imageset/Symptom-ON@3x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/symptoms.imageset/Contents.json b/OCKWatchSample/Assets.xcassets/symptoms.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/symptoms.imageset/Symptom-OFF@1x.png b/OCKWatchSample/Assets.xcassets/symptoms.imageset/Symptom-OFF@1x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/symptoms.imageset/Symptom-OFF@2x.png b/OCKWatchSample/Assets.xcassets/symptoms.imageset/Symptom-OFF@2x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Assets.xcassets/symptoms.imageset/Symptom-OFF@3x.png b/OCKWatchSample/Assets.xcassets/symptoms.imageset/Symptom-OFF@3x.png old mode 100644 new mode 100755 diff --git a/OCKWatchSample/Info.plist b/OCKWatchSample/Info.plist old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 2e266a2..fad50fb --- a/README.md +++ b/README.md @@ -1,11 +1,58 @@ -# CareKitSample+ParseCareKit -![Swift](https://img.shields.io/badge/swift-5.7-brightgreen.svg) ![Xcode 14.0+](https://img.shields.io/badge/xcode-14.0%2B-blue.svg) ![iOS 16.0+](https://img.shields.io/badge/iOS-16.0%2B-blue.svg) ![watchOS 9.0+](https://img.shields.io/badge/watchOS-9.0%2B-blue.svg) ![CareKit 2.1+](https://img.shields.io/badge/CareKit-2.1%2B-red.svg) ![ci](https://github.com/netreconlab/CareKitSample-ParseCareKit/workflows/ci/badge.svg?branch=main) + +# Shower Tracker +![Swift](https://img.shields.io/badge/swift-5.5-brightgreen.svg) ![Xcode 14.2+](https://img.shields.io/badge/xcode-13.2%2B-blue.svg) ![iOS 16.0+](https://img.shields.io/badge/iOS-15.0%2B-blue.svg) ![watchOS 9.0+](https://img.shields.io/badge/watchOS-8.0%2B-blue.svg) ![CareKit 2.1+](https://img.shields.io/badge/CareKit-2.1%2B-red.svg) ![ci](https://github.com/netreconlab/CareKitSample-ParseCareKit/workflows/ci/badge.svg?branch=main) -An example application of [CareKit](https://github.com/carekit-apple/CareKit)'s OCKSample synchronizing CareKit data to the Cloud via [ParseCareKit](https://github.com/netreconlab/ParseCareKit). +## Description + +I created an app called Shower Tracker intended to let users keep track of their shower taking and cleanliness habits. My app allows users to track things such as: shower length, shoulder mobility, water usage, shampoo usage, if they took a shower today, times they washed their face, amount of shampoo and clean towels they have, and how dirty they feel. - +### Demo Video + +Like to the demo video [here](https://www.youtube.com/watch?v=1-cOTB7EfDs) -**Similar to the [What's New in CareKit](https://developer.apple.com/videos/play/wwdc2020/10151/) WWDC20 video, this app syncs between the AppleWatch (setting the flag `isSyncingWithCloud` in `Constants.swift` to `isSyncingWithCloud = false`. Different from the video, setting `isSyncingWithCloud = true` (default behavior) in the aforementioned files syncs iOS and watchOS to a Parse Server.** + +### Designed for the following users + +This app is designed for anyone who takes showers or baths. It will benefit everyone who uses it as they will become more efficient in their shower habbits. It will also benefit the environment as I hope peope who use this app will be more conscious of their water and shampoo usage. + +1 + +2 + +3 + +4 + +5 + +6 + +7 + +8 + +9 + +10 + +11 + + +Developed by: +- Alex Cox(https://github.com/CourtVision1) - `University of Kentucky`, `Computer Science` ParseCareKit synchronizes the following entities to Parse tables/classes using [Parse-Swift](https://github.com/parse-community/Parse-Swift): @@ -19,43 +66,72 @@ ParseCareKit synchronizes the following entities to Parse tables/classes using [ **Use at your own risk. There is no promise that this is HIPAA compliant and we are not responsible for any mishandling of your data** + +## Contributions/Features +* Users can click on the shower picture and they will be redirected to a website with 10 useful shower tips. +* Users can test their shoulder mobility which could be needed for scrubbing all areas. +* Users can do a check in survey to log how long they took a shower and how much shampoo they used. +* Users can track their water usage throughout a day +* Users can keep track if they took a shower on a given day +* Users can keep track if they have clean towels, soap, and shampoo in stock +* Users can keep track of when they wash their face +* Users can keep track of when they feel dirty +* Users can view the data from some of these tasks in the insights tab +* Users can add contacts from their device to the app +* Users can search their contact list in the app +* Users can add information to their own profile and save that information + +## Final Checklist + +- [x] Signup/Login screen tailored to app +- [x] Signup/Login with email address +- [x] Custom app logo +- [x] Custom styling +- [x] Add at least **5 new OCKTask/OCKHealthKitTasks** to your app + - [x] Have a minimum of 7 OCKTask/OCKHealthKitTasks in your app + - [ ] 3/7 of OCKTasks should have different OCKSchedules than what's in the original app +- [x] Use at least 5/7 card below in your app + - [x] InstructionsTaskView - typically used with a OCKTask + - [x] SimpleTaskView - typically used with a OCKTask + - [x] Checklist - typically used with a OCKTask + - [x] Button Log - typically used with a OCKTask + - [x] GridTaskView - typically used with a OCKTask + - [ ] NumericProgressTaskView (SwiftUI) - typically used with a OCKHealthKitTask + - [ ] LabeledValueTaskView (SwiftUI) - typically used with a OCKHealthKitTask +- [x] Add the LinkView (SwiftUI) card to your app +- [x] Replace the current TipView with a class with CustomFeaturedContentView that subclasses OCKFeaturedContentView. This card should have an initializer which takes any link +- [x] Tailor the ResearchKit Onboarding to reflect your application +- [x] Add tailored check-in ResearchKit survey to your app +- [x] Add a new tab called "Insights" to MainTabView +- [x] Replace current ContactView with Searchable contact view +- [x] Change the ProfileView to use a Form view +- [x] Add at least two OCKCarePlan's and tie them to their respective OCKTask's and OCContact's + +## Wishlist features + +1. Before adding Shower Tracker to the app store, I would like to add achievements and badges for completing those achievements to the app. +2. Before adding Shower Tracker to the app store, I would like to add the ability for users to post questions and other users can answer them. +3. Before adding Shower Tracker to the app store, I would like to add pop up notifications to remind people to stay clean or display shower tips + +## Challenges faced while developing + +The biggest challenge I faced while doing this project is working with the existing CareKit codebase. I believe I have a solid enough knowledge of swift and MVVM but I still had a hard time understanding why code was/was not working at times. +Another challenge I faced was trying to get researchKit to work correctly with the on campus computers. It was a big hassle to build researchKit every time I started a new assignment. ## Setup Your Parse Server ### Heroku -The easiest way to setup your server is using the [one-button-click](https://github.com/netreconlab/parse-hipaa#heroku) deployment method for [parse-hipaa](https://github.com/netreconlab/parse-hipaa). - -### Docker -You can setup your [parse-hipaa](https://github.com/netreconlab/parse-hipaa) using Docker. Simply type the following to get parse-hipaa running with postgres locally: - -1. Fork [parse-hipaa](https://github.com/netreconlab/parse-hipaa) -2. `cd parse-hipaa` -3. `docker-compose up` - this will take a couple of minutes to setup as it needs to initialize postgres, but as soon as you see `parse-server running on port 1337.`, it's ready to go. See [here](https://github.com/netreconlab/parse-hipaa#getting-started) for details -4. If you would like to use mongo instead of postgres, in step 3, type `docker-compose -f docker-compose.mongo.yml up` instead of `docker-compose up` - -## Fork this repo to get the modified OCKSample app +The easiest way to setup your server is using the [one-button-click](https://github.com/netreconlab/parse-hipaa#heroku) deplyment method for [parse-hipaa](https://github.com/netreconlab/parse-hipaa). -1. Fork [CareKitSample-ParseCareKit](https://github.com/netreconlab/ParseCareKit) -2. Open `OCKSample.xcodeproj` in Xcode -3. You may need to configure your "Team" and "Bundle Identifier" in "Signing and Capabilities" -4. Run the app and data will synchronize with parse-hipaa via http://localhost:1337/parse automatically -5. You can edit Parse server setup in the ParseCareKit.plist file under "Supporting Files" in the Xcode browser ## View your data in Parse Dashboard ### Heroku -The easiest way to setup your dashboard is using the [one-button-click](https://github.com/netreconlab/parse-hipaa-dashboard#heroku) deployment method for [parse-hipaa-dashboard](https://github.com/netreconlab/parse-hipaa-dashboard). - -### Docker -Parse Dashboard is the easiest way to view your data in the Cloud (or local machine in this example) and comes with [parse-hipaa](https://github.com/netreconlab/parse-hipaa). To access: -1. Open your browser and go to http://localhost:4040/dashboard -2. Username: `parse` -3. Password: `1234` -4. Be sure to refresh your browser to see new changes synched from your CareKitSample app - -Note that CareKit data is extremely sensitive and you are responsible for ensuring your parse-server meets HIPAA compliance. - -## Transitioning the sample app to a production app -If you plan on using this app as a starting point for your produciton app. Once you have your parse-hipaa server in the Cloud behind ssl, you should open `ParseCareKit.plist` in Xcode and change the value for `Server` to point to your server(s) in the Cloud. You should also open `Info.plist` in Xcode and remove `App Transport Security Settings` and any key/value pairs under it as this was only in place to allow you to test the sample app to connect to a server setup on your local machine. iOS apps do not allow non-ssl connections in production, and even if you find a way to connect to non-ssl servers, it would not be HIPAA compliant. - -### Extra scripts for optimized Cloud queries -You should run the extra scripts outlined on parse-hipaa [here](https://github.com/netreconlab/parse-hipaa#running-in-production-for-parsecarekit). +The easiest way to setup your dashboard is using the [one-button-click](https://github.com/netreconlab/parse-hipaa-dashboard#heroku) deplyment method for [parse-hipaa-dashboard](https://github.com/netreconlab/parse-hipaa-dashboard).