diff --git a/OCKSample.xcodeproj/project.pbxproj b/OCKSample.xcodeproj/project.pbxproj index bfa68b64..ed951ded 100644 --- a/OCKSample.xcodeproj/project.pbxproj +++ b/OCKSample.xcodeproj/project.pbxproj @@ -19,7 +19,7 @@ 70221F2F28D7CDE400971195 /* AppDelegate+ParseRemoteDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70221F2D28D7CD9200971195 /* AppDelegate+ParseRemoteDelegate.swift */; }; 70221F3428D8ABBE00971195 /* AppDelegate+UIApplicationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70221F3328D8ABBE00971195 /* AppDelegate+UIApplicationDelegate.swift */; }; 70221F3828D8C0CB00971195 /* AppDelegateKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70F0EFE928C2EE6C0005B5A2 /* AppDelegateKey.swift */; }; - 70221F3928D8C10800971195 /* StoreManagerKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918FDEC4271B4EA70045A0EF /* StoreManagerKey.swift */; }; + 70221F3928D8C10800971195 /* StoreCoordinatorKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918FDEC4271B4EA70045A0EF /* StoreCoordinatorKey.swift */; }; 70308886258273D400FFABB6 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70308885258273D400FFABB6 /* LoginViewModel.swift */; }; 703616FF29CA194900B50BC5 /* CareKitUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = 703616FE29CA194900B50BC5 /* CareKitUtilities */; }; 7036170129CA196800B50BC5 /* CareKitUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = 7036170029CA196800B50BC5 /* CareKitUtilities */; }; @@ -41,6 +41,16 @@ 7083A856279CA40A00B3832E /* PCKUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7083A855279CA40A00B3832E /* PCKUtility.swift */; }; 7083A857279CA40F00B3832E /* PCKUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7083A855279CA40A00B3832E /* PCKUtility.swift */; }; 708542F9276687F90029E888 /* HealthKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51F6343923D2877B00FE576E /* HealthKit.framework */; }; + 7099D1FF29E98D900037CD8E /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7099D1FE29E98D900037CD8E /* AppError.swift */; }; + 7099D20029E98D900037CD8E /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7099D1FE29E98D900037CD8E /* AppError.swift */; }; + 7099D20229E98DDF0037CD8E /* MainViewPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7099D20129E98DDF0037CD8E /* MainViewPath.swift */; }; + 7099D20329E98DDF0037CD8E /* MainViewPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7099D20129E98DDF0037CD8E /* MainViewPath.swift */; }; + 7099D20529E98DFF0037CD8E /* TaskID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7099D20429E98DFF0037CD8E /* TaskID.swift */; }; + 7099D20629E98DFF0037CD8E /* TaskID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7099D20429E98DFF0037CD8E /* TaskID.swift */; }; + 7099D20829E98E200037CD8E /* UserType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7099D20729E98E200037CD8E /* UserType.swift */; }; + 7099D20929E98E200037CD8E /* UserType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7099D20729E98E200037CD8E /* UserType.swift */; }; + 7099D20B29E98E400037CD8E /* InstallationChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7099D20A29E98E400037CD8E /* InstallationChannel.swift */; }; + 7099D20C29E98E400037CD8E /* InstallationChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7099D20A29E98E400037CD8E /* InstallationChannel.swift */; }; 70A98D62278A2683009B58F2 /* Styler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9169381A271B64E100A634ED /* Styler.swift */; }; 70A98D63278A268B009B58F2 /* ColorStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9169381C271B650700A634ED /* ColorStyler.swift */; }; 70A98D68278A2DF1009B58F2 /* TintColorKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918FDEC2271B4E950045A0EF /* TintColorKey.swift */; }; @@ -48,7 +58,6 @@ 70CF66E428E1E74C00FAE977 /* TintColorFlipKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70CF66E328E1E74C00FAE977 /* TintColorFlipKey.swift */; }; 70CF66E528E1E74C00FAE977 /* TintColorFlipKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70CF66E328E1E74C00FAE977 /* TintColorFlipKey.swift */; }; 70DFD80B2567074500B9DB12 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70BD2B1E254B44DB0030B424 /* LoginView.swift */; }; - 70F03A912786073300E5AFB4 /* CareViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70F03A902786073300E5AFB4 /* CareViewModel.swift */; }; 70F03A952786093B00E5AFB4 /* OCKHealthKitPassthroughStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70F03A942786093B00E5AFB4 /* OCKHealthKitPassthroughStore.swift */; }; 70F03A972786098F00E5AFB4 /* OCKStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70F03A962786098F00E5AFB4 /* OCKStore.swift */; }; 70F03A9C27860A2000E5AFB4 /* OCKStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70F03A962786098F00E5AFB4 /* OCKStore.swift */; }; @@ -79,7 +88,7 @@ 918FDEB8271B49060045A0EF /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918FDEB6271B41FF0045A0EF /* Logger.swift */; }; 918FDEB9271B493A0045A0EF /* Installation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918FDEB4271B40590045A0EF /* Installation.swift */; }; 918FDEC3271B4E950045A0EF /* TintColorKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918FDEC2271B4E950045A0EF /* TintColorKey.swift */; }; - 918FDEC5271B4EA70045A0EF /* StoreManagerKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918FDEC4271B4EA70045A0EF /* StoreManagerKey.swift */; }; + 918FDEC5271B4EA70045A0EF /* StoreCoordinatorKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918FDEC4271B4EA70045A0EF /* StoreCoordinatorKey.swift */; }; 91AD922224A45A3900925D4D /* ParseCareKit.plist in Resources */ = {isa = PBXBuildFile; fileRef = 91AD922124A45A3900925D4D /* ParseCareKit.plist */; }; 91AD922424A461D200925D4D /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 91AD922324A461D200925D4D /* README.md */; }; 91AD922D24A4C42D00925D4D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 91AD922C24A4C42D00925D4D /* Assets.xcassets */; }; @@ -166,9 +175,13 @@ 707CC712254DA91900116728 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; 707CC713254DA91900116728 /* OCKLocalization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OCKLocalization.swift; sourceTree = ""; }; 7083A855279CA40A00B3832E /* PCKUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PCKUtility.swift; sourceTree = ""; }; + 7099D1FE29E98D900037CD8E /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; + 7099D20129E98DDF0037CD8E /* MainViewPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewPath.swift; sourceTree = ""; }; + 7099D20429E98DFF0037CD8E /* TaskID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskID.swift; sourceTree = ""; }; + 7099D20729E98E200037CD8E /* UserType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserType.swift; sourceTree = ""; }; + 7099D20A29E98E400037CD8E /* InstallationChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationChannel.swift; sourceTree = ""; }; 70BD2B1E254B44DB0030B424 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; 70CF66E328E1E74C00FAE977 /* TintColorFlipKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TintColorFlipKey.swift; sourceTree = ""; }; - 70F03A902786073300E5AFB4 /* CareViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CareViewModel.swift; sourceTree = ""; }; 70F03A942786093B00E5AFB4 /* OCKHealthKitPassthroughStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKHealthKitPassthroughStore.swift; sourceTree = ""; }; 70F03A962786098F00E5AFB4 /* OCKStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCKStore.swift; sourceTree = ""; }; 70F03AA227860AFF00E5AFB4 /* OCKPatient+Parse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCKPatient+Parse.swift"; sourceTree = ""; }; @@ -186,7 +199,7 @@ 918FDEB4271B40590045A0EF /* Installation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Installation.swift; sourceTree = ""; }; 918FDEB6271B41FF0045A0EF /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 918FDEC2271B4E950045A0EF /* TintColorKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TintColorKey.swift; sourceTree = ""; }; - 918FDEC4271B4EA70045A0EF /* StoreManagerKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreManagerKey.swift; sourceTree = ""; }; + 918FDEC4271B4EA70045A0EF /* StoreCoordinatorKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreCoordinatorKey.swift; sourceTree = ""; }; 91AD922124A45A3900925D4D /* ParseCareKit.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = ParseCareKit.plist; sourceTree = ""; }; 91AD922324A461D200925D4D /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 91AD922724A4C42B00925D4D /* OCKWatchSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OCKWatchSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -357,11 +370,19 @@ path = Localization; sourceTree = ""; }; + 7099D1FD29E98D780037CD8E /* Parse */ = { + isa = PBXGroup; + children = ( + 918FDEB4271B40590045A0EF /* Installation.swift */, + 70077596252228E900EC0EDA /* User.swift */, + ); + path = Parse; + sourceTree = ""; + }; 70A98D64278A2722009B58F2 /* Care */ = { isa = PBXGroup; children = ( 91AD923A24A4C42D00925D4D /* CareView.swift */, - 70F03A902786073300E5AFB4 /* CareViewModel.swift */, ); path = Care; sourceTree = ""; @@ -409,8 +430,12 @@ 918FDEB3271B402F0045A0EF /* Models */ = { isa = PBXGroup; children = ( - 918FDEB4271B40590045A0EF /* Installation.swift */, - 70077596252228E900EC0EDA /* User.swift */, + 7099D1FE29E98D900037CD8E /* AppError.swift */, + 7099D20A29E98E400037CD8E /* InstallationChannel.swift */, + 7099D20129E98DDF0037CD8E /* MainViewPath.swift */, + 7099D20429E98DFF0037CD8E /* TaskID.swift */, + 7099D20729E98E200037CD8E /* UserType.swift */, + 7099D1FD29E98D780037CD8E /* Parse */, ); path = Models; sourceTree = ""; @@ -435,7 +460,7 @@ 70F0EFE928C2EE6C0005B5A2 /* AppDelegateKey.swift */, 70F921A727CA9A3A00368CEC /* CustomStylerKey.swift */, 70F03AA727860E7700E5AFB4 /* FontColorKey.swift */, - 918FDEC4271B4EA70045A0EF /* StoreManagerKey.swift */, + 918FDEC4271B4EA70045A0EF /* StoreCoordinatorKey.swift */, 918FDEC2271B4E950045A0EF /* TintColorKey.swift */, 70CF66E328E1E74C00FAE977 /* TintColorFlipKey.swift */, ); @@ -593,8 +618,9 @@ E72B2BFE226939E3009A9438 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1200; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = "Network Reconnaissance Lab"; TargetAttributes = { 5173CB8623C3A846007655A0 = { @@ -708,11 +734,13 @@ buildActionMask = 2147483647; files = ( 70F921B327CAC16E00368CEC /* LocalSyncSessionDelegate.swift in Sources */, - 70221F3928D8C10800971195 /* StoreManagerKey.swift in Sources */, + 70221F3928D8C10800971195 /* StoreCoordinatorKey.swift in Sources */, + 7099D20029E98D900037CD8E /* AppError.swift in Sources */, 70A98D63278A268B009B58F2 /* ColorStyler.swift in Sources */, 918FDEB8271B49060045A0EF /* Logger.swift in Sources */, 91AD923B24A4C42D00925D4D /* CareView.swift in Sources */, 70F921A927CA9A4000368CEC /* CustomStylerKey.swift in Sources */, + 7099D20629E98DFF0037CD8E /* TaskID.swift in Sources */, 7075151E28DE1A8300A57A0C /* MainView.swift in Sources */, 70A98D62278A2683009B58F2 /* Styler.swift in Sources */, 91693818271B5E1600A634ED /* Constants.swift in Sources */, @@ -724,10 +752,11 @@ 70F03AA627860B2500E5AFB4 /* OCKPatient+Parse.swift in Sources */, 70221F3828D8C0CB00971195 /* AppDelegateKey.swift in Sources */, 9103AA5227B8C913002C921E /* FontColorKey.swift in Sources */, + 7099D20329E98DDF0037CD8E /* MainViewPath.swift in Sources */, 707CC719254DA91900116728 /* OCKLocalization.swift in Sources */, 7007759B252229C900EC0EDA /* User.swift in Sources */, - 70F03A912786073300E5AFB4 /* CareViewModel.swift in Sources */, 70A98D68278A2DF1009B58F2 /* TintColorKey.swift in Sources */, + 7099D20929E98E200037CD8E /* UserType.swift in Sources */, 918FDEB9271B493A0045A0EF /* Installation.swift in Sources */, 91AD923F24A4C42D00925D4D /* NotificationController.swift in Sources */, 70CF66E528E1E74C00FAE977 /* TintColorFlipKey.swift in Sources */, @@ -735,6 +764,7 @@ 70C0D474279BA492003DA141 /* Utility.swift in Sources */, 91AD923D24A4C42D00925D4D /* AppDelegate.swift in Sources */, 91AD924124A4C42D00925D4D /* NotificationView.swift in Sources */, + 7099D20C29E98E400037CD8E /* InstallationChannel.swift in Sources */, 70308886258273D400FFABB6 /* LoginViewModel.swift in Sources */, 70F921B127CAC16E00368CEC /* SessionDelegate.swift in Sources */, ); @@ -746,9 +776,11 @@ files = ( E7440E4F229477F7007AD30A /* CareViewController.swift in Sources */, 918FDEB5271B40590045A0EF /* Installation.swift in Sources */, + 7099D20829E98E200037CD8E /* UserType.swift in Sources */, 70F921AC27CABE3000368CEC /* SessionDelegate.swift in Sources */, 70F0EFEA28C2EE6C0005B5A2 /* AppDelegateKey.swift in Sources */, 7036E64025717F85006E9A3C /* Constants.swift in Sources */, + 7099D20529E98DFF0037CD8E /* TaskID.swift in Sources */, 7036E4CE256E9A0C006E9A3C /* ContactView.swift in Sources */, 70F03A972786098F00E5AFB4 /* OCKStore.swift in Sources */, 70DFD80B2567074500B9DB12 /* LoginView.swift in Sources */, @@ -764,11 +796,13 @@ 918FDEC3271B4E950045A0EF /* TintColorKey.swift in Sources */, 70CF66E428E1E74C00FAE977 /* TintColorFlipKey.swift in Sources */, 70F03AA327860AFF00E5AFB4 /* OCKPatient+Parse.swift in Sources */, + 7099D1FF29E98D900037CD8E /* AppError.swift in Sources */, 70F0EFE828C2EC050005B5A2 /* OCKSampleApp.swift in Sources */, 918FDEB7271B41FF0045A0EF /* Logger.swift in Sources */, + 7099D20229E98DDF0037CD8E /* MainViewPath.swift in Sources */, 70221F2A28D7BE0600971195 /* AppDelegate+ParseRemoteDelegate.swift in Sources */, 7036E4D3256EBE35006E9A3C /* MainView.swift in Sources */, - 918FDEC5271B4EA70045A0EF /* StoreManagerKey.swift in Sources */, + 918FDEC5271B4EA70045A0EF /* StoreCoordinatorKey.swift in Sources */, 91693822271B897200A634ED /* Utility.swift in Sources */, 707CC718254DA91900116728 /* OCKLocalization.swift in Sources */, E7C37849228F887800E982D8 /* TipView.swift in Sources */, @@ -779,6 +813,7 @@ 70F03A952786093B00E5AFB4 /* OCKHealthKitPassthroughStore.swift in Sources */, 7036E4BF256DA089006E9A3C /* LoginViewModel.swift in Sources */, 7036E517256F2413006E9A3C /* ProfileViewModel.swift in Sources */, + 7099D20B29E98E400037CD8E /* InstallationChannel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1194,7 +1229,7 @@ repositoryURL = "https://github.com/cbaker6/CareKit.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 2.1.8; + minimumVersion = "3.0.0-alpha.22"; }; }; 703616FD29CA194900B50BC5 /* XCRemoteSwiftPackageReference "CareKitUtilities" */ = { @@ -1202,7 +1237,7 @@ repositoryURL = "https://github.com/netreconlab/CareKitUtilities.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.0.1; + minimumVersion = "1.0.0-alpha.2"; }; }; 918FDEAD271B3F8F0045A0EF /* XCRemoteSwiftPackageReference "ParseCareKit" */ = { @@ -1210,7 +1245,7 @@ repositoryURL = "https://github.com/netreconlab/ParseCareKit.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.13.1; + minimumVersion = "1.0.0-alpha.80"; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/OCKSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/OCKSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f6e1dc08..8b7ca723 100644 --- a/OCKSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/OCKSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/cbaker6/CareKit.git", "state" : { - "revision" : "954185b307222431e50f656d2870019e9ec97c28", - "version" : "2.1.8" + "revision" : "698176a292e3077f7a30496c42f9e8392328930e", + "version" : "3.0.0-alpha.22" } }, { @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/netreconlab/CareKitUtilities.git", "state" : { - "revision" : "ca65485fd95f40ae1bc27f61ee062b2688ad2554", - "version" : "0.0.1" + "revision" : "7c7be19c8f652589fe97692df92cd7ffb634c41c", + "version" : "1.0.0-alpha.2" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/netreconlab/Parse-Swift.git", "state" : { - "revision" : "8c7d8dce114571052d07516e8f4aef90891960aa", - "version" : "5.3.3" + "revision" : "a746db0bf4e9a2b3b33bc0a7f9dbcaf79feb0c8b", + "version" : "5.4.2" } }, { @@ -41,8 +41,26 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/netreconlab/ParseCareKit.git", "state" : { - "revision" : "57aaabd1dfe0d038ffc40e95239b2445ed832d74", - "version" : "0.13.1" + "revision" : "566f68ef230e4285eb9e19a3d1045947d5a74eef", + "version" : "1.0.0-alpha.80" + } + }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms", + "state" : { + "revision" : "9cfed92b026c524674ed869a4ff2dcfdeedf8a2a", + "version" : "0.1.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", + "version" : "1.0.4" } } ], diff --git a/OCKSample.xcodeproj/xcshareddata/xcschemes/OCKSample.xcscheme b/OCKSample.xcodeproj/xcshareddata/xcschemes/OCKSample.xcscheme index 41cc0380..6710cc15 100644 --- a/OCKSample.xcodeproj/xcshareddata/xcschemes/OCKSample.xcscheme +++ b/OCKSample.xcodeproj/xcshareddata/xcschemes/OCKSample.xcscheme @@ -1,6 +1,6 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OCKSample.xcodeproj/xcshareddata/xcschemes/OCKWatchSample.xcscheme b/OCKSample.xcodeproj/xcshareddata/xcschemes/OCKWatchSample.xcscheme new file mode 100644 index 00000000..cd12f7cb --- /dev/null +++ b/OCKSample.xcodeproj/xcshareddata/xcschemes/OCKWatchSample.xcscheme @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OCKSample/AppDelegate.swift b/OCKSample/AppDelegate.swift index 7e06c56d..3553d75f 100644 --- a/OCKSample/AppDelegate.swift +++ b/OCKSample/AppDelegate.swift @@ -38,91 +38,91 @@ import WatchConnectivity class AppDelegate: UIResponder, ObservableObject { // MARK: Public read/write properties - @Published var isFirstTimeLogin = false { - willSet { - DispatchQueue.main.async { - self.objectWillChange.send() - } - } - } + + @Published var isFirstTimeLogin = false // MARK: Public read private write properties - // swiftlint:disable:next line_length - @Published private(set) var storeManager: OCKSynchronizedStoreManager = .init(wrapping: OCKStore(name: Constants.noCareStoreName, - type: .inMemory)) { + + @Published private(set) var storeCoordinator: OCKStoreCoordinator = .init() { willSet { - StoreManagerKey.defaultValue = newValue - DispatchQueue.main.async { - self.objectWillChange.send() - } + StoreCoordinatorKey.defaultValue = newValue + self.objectWillChange.send() } } - private(set) var parseRemote: ParseRemote! - private(set) var store: OCKStore? + @Published private(set) var store: OCKStore! = .init(name: Constants.noCareStoreName, type: .inMemory) private(set) var healthKitStore: OCKHealthKitPassthroughStore! + private(set) var parseRemote: ParseRemote! // MARK: Private read/write properties + private var sessionDelegate: SessionDelegate! private lazy var watchRemote = OCKWatchConnectivityPeer() // MARK: Helpers + + @MainActor func resetAppToInitialState() { do { - try healthKitStore.reset() + try storeCoordinator.reset() } catch { - Logger.appDelegate.error("Error deleting HealthKit Store: \(error)") + Logger.appDelegate.error("Could not delete Coordinator Store: \(error)") } + do { - try store?.delete() // Delete data in local OCKStore database + try self.store?.delete() } catch { - Logger.appDelegate.error("Error deleting OCKStore: \(error)") + Logger.utility.error("Could not delete local OCKStore because of error: \(error)") } - storeManager = .init(wrapping: OCKStore(name: Constants.noCareStoreName, type: .inMemory)) + + storeCoordinator = .init() healthKitStore = nil parseRemote = nil - store = nil + + let store = OCKStore(name: Constants.noCareStoreName, + type: .inMemory) sessionDelegate.store = store + self.store = store + PCKUtility.removeCache() } + @MainActor func setupRemotes(uuid: UUID? = nil) async throws { do { - if isSyncingWithCloud { + if isSyncingWithRemote { guard let uuid = uuid else { - Logger.appDelegate.error("Error in setupRemotes, uuid is nil") + Logger.appDelegate.error("Could not setupRemotes, uuid is nil") return } parseRemote = try await ParseRemote(uuid: uuid, auto: false, - subscribeToServerUpdates: true, - defaultACL: try? ParseACL.defaultACL()) - store = OCKStore(name: Constants.iOSParseCareStoreName, - type: .onDisk(), - remote: parseRemote) + subscribeToRemoteUpdates: true, + defaultACL: PCKUtility.getDefaultACL()) + let store = OCKStore(name: Constants.iOSParseCareStoreName, + type: .onDisk(), + remote: parseRemote) parseRemote?.parseRemoteDelegate = self sessionDelegate = RemoteSessionDelegate(store: store) + self.store = store } else { - store = OCKStore(name: Constants.iOSLocalCareStoreName, - type: .onDisk(), - remote: watchRemote) + let store = OCKStore(name: Constants.iOSLocalCareStoreName, + type: .onDisk(), + remote: watchRemote) watchRemote.delegate = self sessionDelegate = LocalSessionDelegate(remote: watchRemote, store: store) + self.store = store } // Setup communication with watch WCSession.default.delegate = sessionDelegate WCSession.default.activate() - guard let currentStore = store else { - Logger.appDelegate.error("Should have OCKStore") - return - } - healthKitStore = OCKHealthKitPassthroughStore(store: currentStore) - let coordinator = OCKStoreCoordinator() - coordinator.attach(store: currentStore) - coordinator.attach(eventStore: healthKitStore) - storeManager = OCKSynchronizedStoreManager(wrapping: coordinator) + healthKitStore = OCKHealthKitPassthroughStore(store: store) + let storeCoordinator = OCKStoreCoordinator() + storeCoordinator.attach(store: store) + storeCoordinator.attach(eventStore: healthKitStore) + self.storeCoordinator = storeCoordinator } catch { - Logger.appDelegate.error("Error setting up remote: \(error)") + Logger.appDelegate.error("Could not setup remote: \(error)") throw error } } diff --git a/OCKSample/Constants.swift b/OCKSample/Constants.swift index b605439a..9318c57c 100644 --- a/OCKSample/Constants.swift +++ b/OCKSample/Constants.swift @@ -14,7 +14,8 @@ import ParseSwift /** Set to **true** to sync with ParseServer, *false** to sync with iOS/watchOS. */ -let isSyncingWithCloud = true +let isSyncingWithRemote = true + /** Set to **true** to use WCSession to notify watchOS about updates, **false** to not notify. A change in watchOS 9 removes the ability to use Websockets on real Apple Watches, @@ -23,46 +24,6 @@ let isSyncingWithCloud = true */ let isSendingPushUpdatesToWatch = true -enum AppError: Error { - case couldntCast - case couldntBeUnwrapped - case valueNotFoundInUserInfo - case remoteClockIDNotAvailable - case emptyTaskEvents - case invalidIndexPath(_ indexPath: IndexPath) - case noOutcomeValueForEvent(_ event: OCKAnyEvent, index: Int) - case cannotMakeOutcomeFor(_ event: OCKAnyEvent) - case parseError(_ error: ParseError) - case error(_ error: Error) - case errorString(_ string: String) -} - -extension AppError: LocalizedError { - public var errorDescription: String? { - switch self { - case .couldntCast: - return NSLocalizedString("OCKSampleError: Could not cast to required type.", - comment: "Casting error") - case .couldntBeUnwrapped: - return NSLocalizedString("OCKSampleError: Could not unwrap a required type.", - comment: "Unwrapping error") - case .valueNotFoundInUserInfo: - return NSLocalizedString("OCKSampleError: Could not find the required value in userInfo.", - comment: "Value not found error") - case .remoteClockIDNotAvailable: - return NSLocalizedString("OCKSampleError: Could not get remote clock ID.", - comment: "Value not available error") - case .emptyTaskEvents: return "Task events is empty" - case let .noOutcomeValueForEvent(event, index): return "Event has no outcome value at index \(index): \(event)" - case .invalidIndexPath(let indexPath): return "Invalid index path \(indexPath)" - case .cannotMakeOutcomeFor(let event): return "Cannot make outcome for event: \(event)" - case .parseError(let error): return "\(error)" - case .error(let error): return "\(error)" - case .errorString(let string): return string - } - } -} - enum Constants { static let parseConfigFileName = "ParseCareKit" static let iOSParseCareStoreName = "iOSParseStore" @@ -76,37 +37,6 @@ enum Constants { static let finishedAskingForPermission = "finishedAskingForPermission" static let shouldRefreshView = "shouldRefreshView" static let userLoggedIn = "userLoggedIn" - static let storeInitialized = "storeInitialized" static let userTypeKey = "userType" -} - -enum MainViewPath { - case tabs -} - -enum TaskID { - static let doxylamine = "doxylamine" - static let nausea = "nausea" - static let stretch = "stretch" - static let kegels = "kegels" - static let steps = "steps" - - static var ordered: [String] { - [Self.steps, Self.doxylamine, Self.kegels, Self.stretch, Self.nausea] - } -} - -enum UserType: String, Codable { - case patient = "Patient" - case none = "None" - - // Return all types as an array, make sure to maintain order above - func allTypesAsArray() -> [String] { - return [UserType.patient.rawValue, - UserType.none.rawValue] - } -} - -enum InstallationChannel: String { - case global + static let appName = "ParseCareKitSample" } diff --git a/OCKSample/Environment/StoreCoordinatorKey.swift b/OCKSample/Environment/StoreCoordinatorKey.swift new file mode 100644 index 00000000..9cbf20fc --- /dev/null +++ b/OCKSample/Environment/StoreCoordinatorKey.swift @@ -0,0 +1,29 @@ +// +// StoreCoordinatorKey.swift +// OCKSample +// +// Created by Corey Baker on 10/16/21. +// Copyright © 2021 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation +import SwiftUI +import CareKit +import CareKitStore + +struct StoreCoordinatorKey: EnvironmentKey { + static var defaultValue = OCKStoreCoordinator() +} + +extension EnvironmentValues { + + var storeCoordinator: OCKStoreCoordinator { + get { + self[StoreCoordinatorKey.self] + } + + set { + self[StoreCoordinatorKey.self] = newValue + } + } +} diff --git a/OCKSample/Environment/StoreManagerKey.swift b/OCKSample/Environment/StoreManagerKey.swift deleted file mode 100644 index d7dbc78c..00000000 --- a/OCKSample/Environment/StoreManagerKey.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// StoreManagerKey.swift -// OCKSample -// -// Created by Corey Baker on 10/16/21. -// Copyright © 2021 Network Reconnaissance Lab. All rights reserved. -// - -import Foundation -import SwiftUI -import CareKit -import CareKitStore - -struct StoreManagerKey: EnvironmentKey { - static var defaultValue = OCKSynchronizedStoreManager(wrapping: OCKStore(name: Constants.noCareStoreName, - type: .inMemory)) -} - -extension EnvironmentValues { - - var storeManager: OCKSynchronizedStoreManager { - get { - self[StoreManagerKey.self] - } - - set { - self[StoreManagerKey.self] = newValue - } - } -} diff --git a/OCKSample/Extensions/AppDelegate+ParseRemoteDelegate.swift b/OCKSample/Extensions/AppDelegate+ParseRemoteDelegate.swift index 86388db7..5f8c72ec 100644 --- a/OCKSample/Extensions/AppDelegate+ParseRemoteDelegate.swift +++ b/OCKSample/Extensions/AppDelegate+ParseRemoteDelegate.swift @@ -17,21 +17,29 @@ extension AppDelegate: ParseRemoteDelegate { NotificationCenter.default.post(.init(name: Notification.Name(rawValue: Constants.requestSync))) } - func successfullyPushedDataToCloud() { + @MainActor + func successfullyPushedToRemote() { if isFirstTimeLogin { + // BAKER: @MainActor not working (shows purple warning), leave async. DispatchQueue.main.async { self.isFirstTimeLogin.toggle() } } #if !targetEnvironment(simulator) // watchOS 9 needs to be sent messages for updates on real devices - let message = Utility.prepareSyncMessageForWatch() - WCSession.default.sendMessage(message, - replyHandler: nil, - errorHandler: nil) + if isSendingPushUpdatesToWatch { + let message = Utility.prepareSyncMessageForWatch() + WCSession.default.sendMessage(message, + replyHandler: nil, + errorHandler: nil) + } #endif } + func provideStore() -> OCKAnyStoreProtocol { + return storeCoordinator + } + func remote(_ remote: OCKRemoteSynchronizable, didUpdateProgress progress: Double) { let progressPercentage = Int(progress * 100.0) NotificationCenter.default.post(.init(name: Notification.Name(rawValue: Constants.progressUpdate), @@ -40,28 +48,14 @@ extension AppDelegate: ParseRemoteDelegate { func chooseConflictResolution(conflicts: [OCKEntity], completion: @escaping OCKResultClosure) { // https://github.com/carekit-apple/CareKit/issues/567 - // Workaround to handle deleted and re-added outcomes. - // Always prefer updates over deletes. - let outcomes = conflicts.compactMap { conflict -> OCKOutcome? in - if case let .outcome(outcome) = conflict { - return outcome - } else { - return nil - } - } - - if outcomes.count == 2, - outcomes.contains(where: { $0.deletedDate != nil}), - let added = outcomes.first(where: { $0.deletedDate == nil}) { - - completion(.success(.outcome(added))) - return - } - - if let first = conflicts.first { - completion(.success(first)) - } else { - completion(.failure(.remoteSynchronizationFailed(reason: "Error, none selected for conflict"))) + // Last write wins + do { + let lastWrite = try conflicts + .max(by: { try $0.parseEntity().value.createdDate! > $1.parseEntity().value.createdDate! })! + + completion(.success(lastWrite)) + } catch { + completion(.failure(.invalidValue(reason: error.localizedDescription))) } } } diff --git a/OCKSample/Extensions/AppDelegate+UIApplicationDelegate.swift b/OCKSample/Extensions/AppDelegate+UIApplicationDelegate.swift index 4ff89c16..52e8aeb2 100644 --- a/OCKSample/Extensions/AppDelegate+UIApplicationDelegate.swift +++ b/OCKSample/Extensions/AppDelegate+UIApplicationDelegate.swift @@ -14,16 +14,18 @@ extension AppDelegate: UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { Task { - do { - // Parse-Server setup - try await PCKUtility.setupServer(fileName: Constants.parseConfigFileName) { _, completionHandler in - completionHandler(.performDefaultHandling, nil) + if isSyncingWithRemote { + do { + // Parse-Server setup + // swiftlint:disable:next line_length + try await PCKUtility.configureParse(fileName: Constants.parseConfigFileName) { _, completionHandler in + completionHandler(.performDefaultHandling, nil) + } + } catch { + Logger.appDelegate.info("Could not configure Parse Swift: \(error)") + return } - } catch { - Logger.appDelegate.info("Could not configure Parse Swift: \(error)") - return - } - if isSyncingWithCloud { + await Utility.clearDeviceOnFirstRun() do { _ = try await User.current() Logger.appDelegate.info("User is already signed in...") @@ -43,10 +45,11 @@ extension AppDelegate: UIApplicationDelegate { Logger.appDelegate.error("User is not loggied in: \(error)") } } else { + await Utility.clearDeviceOnFirstRun() // When syncing directly with watchOS, we do not care about login and need to setup remotes do { try await setupRemotes() - try await store?.populateSampleData() + try await store.populateSampleData() try await healthKitStore.populateSampleData() DispatchQueue.main.asyncAfter(deadline: .now() + 1) { NotificationCenter.default.post(.init(name: Notification.Name(rawValue: Constants.requestSync))) @@ -54,9 +57,9 @@ extension AppDelegate: UIApplicationDelegate { } } catch { Logger.appDelegate.error(""" - Error in SceneDelage, could not populate - data stores: \(error) - """) + Could not populate + data stores: \(error) + """) } } } diff --git a/OCKSample/Main/Care/CareView.swift b/OCKSample/Main/Care/CareView.swift index 6abfa530..13133a88 100644 --- a/OCKSample/Main/Care/CareView.swift +++ b/OCKSample/Main/Care/CareView.swift @@ -8,14 +8,21 @@ // swiftlint:disable:next line_length // This file embeds a UIKit View Controller inside of a SwiftUI view. I used this tutorial to figure this out https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit -import SwiftUI -import UIKit import CareKit import CareKitStore import os.log +import SwiftUI +import UIKit struct CareView: UIViewControllerRepresentable { - @EnvironmentObject private var appDelegate: AppDelegate + private static var query: OCKEventQuery { + var query = OCKEventQuery(for: Date()) + query.taskIDs = [TaskID.steps] + return query + } + @Environment(\.appDelegate) private var appDelegate + @Environment(\.careStore) private var careStore + @CareStoreFetchRequest(query: query) private var events func makeUIViewController(context: Context) -> some UIViewController { let viewController = createViewController() @@ -26,15 +33,23 @@ struct CareView: UIViewControllerRepresentable { func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { - guard let navigationController = uiViewController as? UINavigationController else { - Logger.feed.error("View should have been a UINavigationController") + guard let navigationController = uiViewController as? UINavigationController, + let careViewController = navigationController.viewControllers.first as? CareViewController else { + Logger.feed.error("CareView should have been a UINavigationController") + return + } + guard careViewController.store !== careStore || + appDelegate?.isFirstTimeLogin == true else { + // No need to replace view + // careViewController.events = events return } navigationController.setViewControllers([createViewController()], animated: false) } func createViewController() -> UIViewController { - CareViewController(storeManager: appDelegate.storeManager) + CareViewController(store: careStore, + events: events) } } @@ -42,5 +57,7 @@ struct CareView_Previews: PreviewProvider { static var previews: some View { CareView() .accentColor(Color(TintColorKey.defaultValue)) + .environment(\.appDelegate, AppDelegate()) + .environment(\.careStore, Utility.createPreviewStore()) } } diff --git a/OCKSample/Main/Care/CareViewController.swift b/OCKSample/Main/Care/CareViewController.swift index c319f531..171812cf 100644 --- a/OCKSample/Main/Care/CareViewController.swift +++ b/OCKSample/Main/Care/CareViewController.swift @@ -28,19 +28,36 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import Foundation -import UIKit -import SwiftUI -import Combine import CareKit import CareKitStore import CareKitUI import os.log +import SwiftUI +import UIKit class CareViewController: OCKDailyPageViewController { private var isSyncing = false private var isLoading = false + var events: CareStoreFetchedResults? { + didSet { + self.reloadView() + } + } + + /// Create an instance of the view controller. Will hook up the calendar to the tasks collection, + /// and query and display the tasks. + /// + /// - Parameter store: The store from which to query the tasks. + /// - Parameter computeProgress: Used to compute the combined progress for a series of CareKit events. + init(store: OCKAnyStoreProtocol, + events: CareStoreFetchedResults? = nil, + computeProgress: @escaping (OCKAnyEvent) -> CareTaskProgress = { event in + event.computeProgress(by: .checkingOutcomeExists) + }) { + super.init(store: store, computeProgress: computeProgress) + // self.events = events + } override func viewDidLoad() { super.viewDidLoad() @@ -102,7 +119,7 @@ class CareViewController: OCKDailyPageViewController { return } isSyncing = true - AppDelegateKey.defaultValue?.store?.synchronize { error in + AppDelegateKey.defaultValue?.store.synchronize { error in let errorString = error?.localizedDescription ?? "Successful sync with remote!" Logger.feed.info("\(errorString)") DispatchQueue.main.async { @@ -149,60 +166,69 @@ class CareViewController: OCKDailyPageViewController { } } - Task { - let tasks = await self.fetchTasks(on: date) - tasks.compactMap { - let cards = self.taskViewController(for: $0, on: date) - cards?.forEach { - if let carekitView = $0.view as? OCKView { - carekitView.customStyle = CustomStylerKey.defaultValue + fetchTasks(on: date) { result in + switch result { + case .success(let tasks): + tasks.compactMap { + let cards = self.taskViewController(for: $0, + on: date) + cards?.forEach { + if let carekitView = $0.view as? OCKView { + carekitView.customStyle = CustomStylerKey.defaultValue + } + $0.view.isUserInteractionEnabled = isCurrentDay + $0.view.alpha = !isCurrentDay ? 0.4 : 1.0 + } + return cards + }.forEach { (cards: [UIViewController]) in + cards.forEach { + listViewController.appendViewController($0, animated: false) } - $0.view.isUserInteractionEnabled = isCurrentDay - $0.view.alpha = !isCurrentDay ? 0.4 : 1.0 - } - return cards - }.forEach { (cards: [UIViewController]) in - cards.forEach { - listViewController.appendViewController($0, animated: false) } + case .failure(let error): + Logger.feed.error("Could not fetch tasks: \(error)") } self.isLoading = false } } + private func getStoreFetchRequestEvent(for taskId: String) -> CareStoreFetchedResult? { + events?.filter({ $0.result.task.id == taskId }).last + } + private func taskViewController(for task: OCKAnyTask, on date: Date) -> [UIViewController]? { + + var query = OCKEventQuery(for: Date()) + query.taskIDs = [task.id] + switch task.id { case TaskID.steps: - let view = NumericProgressTaskView( - task: task, - eventQuery: OCKEventQuery(for: date), - storeManager: self.storeManager) - .padding([.vertical], 20) + guard let event = getStoreFetchRequestEvent(for: task.id) else { + return nil + } + let view = NumericProgressTaskView(event: event) .careKitStyle(CustomStylerKey.defaultValue) return [view.formattedHostingController()] + case TaskID.stretch: - return [OCKInstructionsTaskViewController(task: task, - eventQuery: .init(for: date), - storeManager: self.storeManager)] + return [OCKInstructionsTaskViewController(query: query, + store: self.store)] case TaskID.kegels: /* 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. */ - return [OCKSimpleTaskViewController(task: task, - eventQuery: .init(for: date), - storeManager: self.storeManager)] + return [OCKSimpleTaskViewController(query: query, + store: self.store)] // Create a card for the doxylamine task if there are events for it on this day. case TaskID.doxylamine: - return [OCKChecklistTaskViewController( - task: task, - eventQuery: .init(for: date), - storeManager: self.storeManager)] + return [OCKChecklistTaskViewController(query: query, + store: self.store)] case TaskID.nausea: var cards = [UIViewController]() @@ -216,26 +242,28 @@ class CareViewController: OCKDailyPageViewController { legendTitle: "Nausea", gradientStartColor: nauseaGradientStart, gradientEndColor: nauseaGradientEnd, - markerSize: 10, - eventAggregator: OCKEventAggregator.countOutcomeValues) + markerSize: 10) { event in + event.computeProgress(by: .summingOutcomeValues) + } let doxylamineDataSeries = OCKDataSeriesConfiguration( taskID: task.id, legendTitle: "Doxylamine", gradientStartColor: .systemGray2, gradientEndColor: .systemGray, - markerSize: 10, - eventAggregator: OCKEventAggregator.countOutcomeValues) + markerSize: 10) { event in + event.computeProgress(by: .summingOutcomeValues) + } let insightsCard = OCKCartesianChartViewController( plotType: .bar, selectedDate: date, configurations: [nauseaDataSeries, doxylamineDataSeries], - storeManager: self.storeManager) + store: self.store) - insightsCard.chartView.headerView.titleLabel.text = "Nausea & Doxylamine Intake" - insightsCard.chartView.headerView.detailLabel.text = "This Week" - insightsCard.chartView.headerView.accessibilityLabel = "Nausea & Doxylamine Intake, This Week" + insightsCard.typedView.headerView.titleLabel.text = "Nausea & Doxylamine Intake" + insightsCard.typedView.headerView.detailLabel.text = "This Week" + insightsCard.typedView.headerView.accessibilityLabel = "Nausea & Doxylamine Intake, This Week" cards.append(insightsCard) /* @@ -243,9 +271,8 @@ class CareViewController: OCKDailyPageViewController { 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, - eventQuery: .init(for: date), - storeManager: self.storeManager) + let nauseaCard = OCKButtonLogTaskViewController(query: query, + store: self.store) cards.append(nauseaCard) return cards @@ -254,17 +281,20 @@ class CareViewController: OCKDailyPageViewController { } } - private func fetchTasks(on date: Date) async -> [OCKAnyTask] { + private func fetchTasks(on date: Date, + completion: @escaping (Result<[OCKAnyTask], Error>) -> Void) { var query = OCKTaskQuery(for: date) 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 - } catch { - Logger.feed.error("\(error, privacy: .public)") - return [] + store.fetchAnyTasks(query: query, callbackQueue: .main) { result in + switch result { + case .success(let tasks): + let orderedTasks = TaskID.ordered.compactMap { orderedTaskID in + tasks.first(where: { $0.id == orderedTaskID }) + } + completion(.success(orderedTasks)) + case .failure(let error): + completion(.failure(error)) + } } } } diff --git a/OCKSample/Main/Contact/ContactView.swift b/OCKSample/Main/Contact/ContactView.swift index aa24f210..203c5317 100644 --- a/OCKSample/Main/Contact/ContactView.swift +++ b/OCKSample/Main/Contact/ContactView.swift @@ -6,14 +6,14 @@ // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. // -import SwiftUI -import UIKit import CareKit import CareKitStore import os.log +import SwiftUI +import UIKit struct ContactView: UIViewControllerRepresentable { - @EnvironmentObject private var appDelegate: AppDelegate + @Environment(\.careStore) var careStore func makeUIViewController(context: Context) -> some UIViewController { let viewController = createViewController() @@ -23,14 +23,15 @@ struct ContactView: UIViewControllerRepresentable { func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { guard let navigationController = uiViewController as? UINavigationController else { - Logger.feed.error("View should have been a UINavigationController") + Logger.feed.error("ContactView should have been a UINavigationController") return } navigationController.setViewControllers([createViewController()], animated: false) } func createViewController() -> UIViewController { - OCKContactsListViewController(storeManager: appDelegate.storeManager) + OCKContactsListViewController(store: careStore, + contactViewSynchronizer: OCKDetailedContactViewSynchronizer()) } } @@ -39,5 +40,6 @@ struct ContactView_Previews: PreviewProvider { static var previews: some View { ContactView() .accentColor(Color(TintColorKey.defaultValue)) + .environment(\.careStore, Utility.createPreviewStore()) } } diff --git a/OCKSample/Main/Login/LoginView.swift b/OCKSample/Main/Login/LoginView.swift index c182c5b8..a902955b 100644 --- a/OCKSample/Main/Login/LoginView.swift +++ b/OCKSample/Main/Login/LoginView.swift @@ -11,8 +11,8 @@ https://www.iosapptemplates.com/blog/swiftui/login-screen-swiftui */ -import SwiftUI import ParseSwift +import SwiftUI import UIKit /* diff --git a/OCKSample/Main/Login/LoginViewModel.swift b/OCKSample/Main/Login/LoginViewModel.swift index 4ea412f1..a88c7036 100644 --- a/OCKSample/Main/Login/LoginViewModel.swift +++ b/OCKSample/Main/Login/LoginViewModel.swift @@ -6,13 +6,12 @@ // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. // -import Foundation -import ParseCareKit -import ParseSwift import CareKit import CareKitStore -import WatchConnectivity +import ParseCareKit +import ParseSwift import os.log +import WatchConnectivity class LoginViewModel: ObservableObject { @@ -39,20 +38,17 @@ class LoginViewModel: ObservableObject { } // MARK: Helpers (private) + @MainActor private func checkStatus() async { let isLoggedOut = self.isLoggedOut do { _ = try await User.current() if isLoggedOut { - DispatchQueue.main.async { - self.isLoggedOut = false - } + self.isLoggedOut = false } } catch { if !isLoggedOut { - DispatchQueue.main.async { - self.isLoggedOut = true - } + self.isLoggedOut = true } } } @@ -121,26 +117,21 @@ class LoginViewModel: ObservableObject { throw AppError.couldntBeUnwrapped } try await appDelegate.setupRemotes(uuid: remoteUUID) - let storeManager = appDelegate.storeManager var newPatient = OCKPatient(remoteUUID: remoteUUID, id: remoteUUID.uuidString, givenName: firstName, familyName: lastName) newPatient.userType = type - let savedPatient = try await storeManager.store.addAnyPatient(newPatient) - guard let patient = savedPatient as? OCKPatient else { - throw AppError.couldntCast - } - - try await appDelegate.store?.populateSampleData() + let savedPatient = try await appDelegate.store.addPatient(newPatient) + try await appDelegate.store.populateSampleData() try await appDelegate.healthKitStore.populateSampleData() appDelegate.parseRemote.automaticallySynchronizes = true // Post notification to sync NotificationCenter.default.post(.init(name: Notification.Name(rawValue: Constants.requestSync))) Logger.login.info("Successfully added a new Patient") - return patient + return savedPatient } // MARK: User intentional behavior diff --git a/OCKSample/Main/MainTabView.swift b/OCKSample/Main/MainTabView.swift index d33625b0..02085f9b 100644 --- a/OCKSample/Main/MainTabView.swift +++ b/OCKSample/Main/MainTabView.swift @@ -8,6 +8,7 @@ // swiftlint:disable:next line_length // This was built using tutorial: https://www.hackingwithswift.com/books/ios-swiftui/creating-tabs-with-tabview-and-tabitem +import CareKitStore import SwiftUI struct MainTabView: View { @@ -52,7 +53,6 @@ struct MainTabView: View { } .tag(2) } - .navigationBarHidden(true) } } @@ -60,5 +60,6 @@ struct MainTabView_Previews: PreviewProvider { static var previews: some View { MainTabView(loginViewModel: .init()) .accentColor(Color(TintColorKey.defaultValue)) + .environment(\.careStore, Utility.createPreviewStore()) } } diff --git a/OCKSample/Main/MainView.swift b/OCKSample/Main/MainView.swift index a956c3f1..9cde6e62 100644 --- a/OCKSample/Main/MainView.swift +++ b/OCKSample/Main/MainView.swift @@ -5,14 +5,16 @@ // Created by Corey Baker on 11/25/20. // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. -import SwiftUI import CareKit import CareKitStore import CareKitUI +import SwiftUI struct MainView: View { - @StateObject var loginViewModel = LoginViewModel() - @State var path = [MainViewPath]() + @EnvironmentObject private var appDelegate: AppDelegate + @StateObject private var loginViewModel = LoginViewModel() + @State private var path = [MainViewPath]() + @State private var storeCoordinator = OCKStoreCoordinator() var body: some View { NavigationStack(path: $path) { @@ -20,16 +22,17 @@ struct MainView: View { .navigationDestination(for: MainViewPath.self) { destination in switch destination { case .tabs: - if isSyncingWithCloud { + if isSyncingWithRemote { MainTabView(loginViewModel: loginViewModel) + .navigationBarHidden(true) } else { CareView() + .navigationBarHidden(true) } } } - .navigationBarHidden(true) .onAppear { - guard isSyncingWithCloud else { + guard isSyncingWithRemote else { path = [.tabs] return } @@ -40,6 +43,7 @@ struct MainView: View { path = [.tabs] } } + .environment(\.careStore, storeCoordinator) .onReceive(loginViewModel.$isLoggedOut, perform: { isLoggedOut in guard !isLoggedOut else { path = [] @@ -47,12 +51,20 @@ struct MainView: View { } path = [.tabs] }) + .onReceive(appDelegate.$storeCoordinator) { newStoreCoordinator in + guard storeCoordinator !== newStoreCoordinator else { + return + } + storeCoordinator = newStoreCoordinator + } } } struct MainView_Previews: PreviewProvider { static var previews: some View { MainView() + .environment(\.appDelegate, AppDelegate()) + .environment(\.careStore, Utility.createPreviewStore()) .accentColor(Color(TintColorKey.defaultValue)) } } diff --git a/OCKSample/Main/Profile/ProfileView.swift b/OCKSample/Main/Profile/ProfileView.swift index d760da7a..928ddbd0 100644 --- a/OCKSample/Main/Profile/ProfileView.swift +++ b/OCKSample/Main/Profile/ProfileView.swift @@ -6,34 +6,36 @@ // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. // -import SwiftUI import CareKitUI import CareKitStore import CareKit import os.log +import SwiftUI struct ProfileView: View { - @EnvironmentObject private var appDelegate: AppDelegate - @StateObject var viewModel = ProfileViewModel() + private static var query = OCKPatientQuery(for: Date()) + @CareStoreFetchRequest(query: query) private var patients + @StateObject private 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) + TextField("First Name", + text: $viewModel.firstName) .padding() .cornerRadius(20.0) .shadow(radius: 10.0, x: 20, y: 10) - TextField("Last Name", text: $lastName) + TextField("Last Name", + text: $viewModel.lastName) .padding() .cornerRadius(20.0) .shadow(radius: 10.0, x: 20, y: 10) - DatePicker("Birthday", selection: $birthday, displayedComponents: [DatePickerComponents.date]) + DatePicker("Birthday", + selection: $viewModel.birthday, + displayedComponents: [DatePickerComponents.date]) .padding() .cornerRadius(20.0) .shadow(radius: 10.0, x: 20, y: 10) @@ -42,9 +44,7 @@ struct ProfileView: View { Button(action: { Task { do { - try await viewModel.saveProfile(firstName, - last: lastName, - birth: birthday) + try await viewModel.saveProfile() } catch { Logger.profile.error("Error saving profile: \(error)") } @@ -74,28 +74,17 @@ struct ProfileView: View { }) .background(Color(.red)) .cornerRadius(15) - }.onReceive(viewModel.$patient) { patient in - if let currentFirstName = patient?.name.givenName { - firstName = currentFirstName - } - if let currentLastName = patient?.name.familyName { - lastName = currentLastName - } - if let currentBirthday = patient?.birthday { - birthday = currentBirthday - } - }.onReceive(appDelegate.$storeManager) { newStoreManager in - viewModel.updateStoreManager(newStoreManager) - }.onReceive(appDelegate.$isFirstTimeLogin) { _ in - viewModel.updateStoreManager() + } + .onReceive(patients.publisher) { publishedPatient in + viewModel.updatePatient(publishedPatient.result) } } } struct ProfileView_Previews: PreviewProvider { static var previews: some View { - ProfileView(viewModel: .init(storeManager: Utility.createPreviewStoreManager()), - loginViewModel: .init()) + ProfileView(loginViewModel: .init()) .accentColor(Color(TintColorKey.defaultValue)) + .environment(\.careStore, Utility.createPreviewStore()) } } diff --git a/OCKSample/Main/Profile/ProfileViewModel.swift b/OCKSample/Main/Profile/ProfileViewModel.swift index 6134067b..d1c7b92e 100644 --- a/OCKSample/Main/Profile/ProfileViewModel.swift +++ b/OCKSample/Main/Profile/ProfileViewModel.swift @@ -6,140 +6,71 @@ // Copyright © 2020 Network Reconnaissance Lab. All rights reserved. // -import Foundation import CareKit import CareKitStore import CareKitUtilities import SwiftUI -import ParseCareKit import os.log -import Combine class ProfileViewModel: ObservableObject { - // MARK: Public read, private write properties - @Published private(set) var patient: OCKPatient? - private(set) var storeManager: OCKSynchronizedStoreManager { - didSet { - reloadViewModel() - } - } - - // MARK: Private read/write properties - private var cancellables: Set = [] - - init(storeManager: OCKSynchronizedStoreManager? = nil) { - self.storeManager = storeManager ?? StoreManagerKey.defaultValue - } - - // MARK: Helpers (private) - private func clearSubscriptions() { - cancellables = [] - } - private func reloadViewModel() { - Task { - _ = await findAndObserveCurrentProfile() - } - } + // MARK: Public read/write properties - func updateStoreManager(_ storeManager: OCKSynchronizedStoreManager? = nil) { - guard let storeManager = storeManager else { - guard let appDelegateStoreManager = AppDelegateKey.defaultValue?.storeManager else { - Logger.profile.error("Missing AppDelegate storeManager") - return + var firstName = "" + var lastName = "" + var birthday = Date() + var patient: OCKPatient? { + willSet { + if let currentFirstName = newValue?.name.givenName { + firstName = currentFirstName + } + if let currentLastName = newValue?.name.familyName { + lastName = currentLastName + } + if let currentBirthday = newValue?.birthday { + birthday = currentBirthday } - self.storeManager = appDelegateStoreManager - return } - self.storeManager = storeManager } - @MainActor - private func findAndObserveCurrentProfile() async { - guard let uuid = try? await Utility.getRemoteClockUUID() else { - Logger.profile.error("Could not get remote uuid for this user") - return - } - clearSubscriptions() + // MARK: Helpers (public) - // 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 { - let foundPatient = try await storeManager.store.fetchAnyPatients(query: queryForCurrentPatient) - guard let currentPatient = foundPatient.first as? OCKPatient 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) - } 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)") + func updatePatient(_ patient: OCKAnyPatient) { + guard let patient = patient as? OCKPatient else { + return } - } - - @MainActor - private func observePatient(_ patient: OCKPatient) { - storeManager.publisher(forPatient: patient, - categories: [.add, .update, .delete]) - .sink { [weak self] in - self?.patient = $0 as? OCKPatient - } - .store(in: &cancellables) + self.patient = patient } // MARK: User intentional behavior - @MainActor - func saveProfile(_ first: String, last: String, birth: Date) 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 + func saveProfile() async throws { - if patient?.name.givenName != first { - patientHasBeenUpdated = true - patientToUpdate.name.givenName = first - } + guard var patientToUpdate = patient else { + throw AppError.errorString("The profile is missing the Patient") + } - if patient?.name.familyName != last { - patientHasBeenUpdated = true - patientToUpdate.name.familyName = last - } + // If there is a currentPatient that was fetched, check to see if any of the fields changed + var patientHasBeenUpdated = false - if patient?.birthday != birth { - patientHasBeenUpdated = true - patientToUpdate.birthday = birth - } - - if patientHasBeenUpdated { - let updated = try await storeManager.store.updateAnyPatient(patientToUpdate) - Logger.profile.info("Successfully updated patient") - guard let updatedPatient = updated as? OCKPatient else { - return - } - self.patient = updatedPatient - } + if patient?.name.givenName != firstName { + patientHasBeenUpdated = true + patientToUpdate.name.givenName = firstName + } - } else { - guard let remoteUUID = try? await Utility.getRemoteClockUUID().uuidString else { - Logger.profile.error("The user currently is not logged in") - return - } + if patient?.name.familyName != lastName { + patientHasBeenUpdated = true + patientToUpdate.name.familyName = lastName + } - var newPatient = OCKPatient(id: remoteUUID, givenName: first, familyName: last) - newPatient.birthday = birth + if patient?.birthday != birthday { + patientHasBeenUpdated = true + patientToUpdate.birthday = birthday + } - // This is new patient that has never been saved before - let addedPatient = try await storeManager.store.addAnyPatient(newPatient) - Logger.profile.info("Succesffully saved new patient") - guard let addedOCKPatient = addedPatient as? OCKPatient else { - Logger.profile.error("Could not cast to OCKPatient") - return - } - self.patient = addedOCKPatient + if patientHasBeenUpdated { + _ = try await AppDelegateKey.defaultValue?.store.updatePatient(patientToUpdate) + Logger.profile.info("Successfully updated patient") } } } diff --git a/OCKSample/Main/Stylers/ColorStyler.swift b/OCKSample/Main/Stylers/ColorStyler.swift index e539fcd0..e89a510e 100644 --- a/OCKSample/Main/Stylers/ColorStyler.swift +++ b/OCKSample/Main/Stylers/ColorStyler.swift @@ -6,7 +6,6 @@ // Copyright © 2021 Network Reconnaissance Lab. All rights reserved. // -import Foundation import CareKitUI import UIKit diff --git a/OCKSample/Main/Stylers/Styler.swift b/OCKSample/Main/Stylers/Styler.swift index 049f58dc..cff44123 100644 --- a/OCKSample/Main/Stylers/Styler.swift +++ b/OCKSample/Main/Stylers/Styler.swift @@ -6,7 +6,6 @@ // Copyright © 2021 Network Reconnaissance Lab. All rights reserved. // -import Foundation import CareKitUI struct Styler: OCKStyler { diff --git a/OCKSample/Models/AppError.swift b/OCKSample/Models/AppError.swift new file mode 100644 index 00000000..977e5c3c --- /dev/null +++ b/OCKSample/Models/AppError.swift @@ -0,0 +1,51 @@ +// +// AppError.swift +// OCKSample +// +// Created by Corey Baker on 4/14/23. +// Copyright © 2023 Network Reconnaissance Lab. All rights reserved. +// + +import CareKitStore +import Foundation +import ParseSwift + +enum AppError: Error { + case couldntCast + case couldntBeUnwrapped + case valueNotFoundInUserInfo + case remoteClockIDNotAvailable + case emptyTaskEvents + case invalidIndexPath(_ indexPath: IndexPath) + case noOutcomeValueForEvent(_ event: OCKAnyEvent, index: Int) + case cannotMakeOutcomeFor(_ event: OCKAnyEvent) + case parseError(_ error: ParseError) + case error(_ error: Error) + case errorString(_ string: String) +} + +extension AppError: LocalizedError { + public var errorDescription: String? { + switch self { + case .couldntCast: + return NSLocalizedString("OCKSampleError: Could not cast to required type.", + comment: "Casting error") + case .couldntBeUnwrapped: + return NSLocalizedString("OCKSampleError: Could not unwrap a required type.", + comment: "Unwrapping error") + case .valueNotFoundInUserInfo: + return NSLocalizedString("OCKSampleError: Could not find the required value in userInfo.", + comment: "Value not found error") + case .remoteClockIDNotAvailable: + return NSLocalizedString("OCKSampleError: Could not get remote clock ID.", + comment: "Value not available error") + case .emptyTaskEvents: return "Task events is empty" + case let .noOutcomeValueForEvent(event, index): return "Event has no outcome value at index \(index): \(event)" + case .invalidIndexPath(let indexPath): return "Invalid index path \(indexPath)" + case .cannotMakeOutcomeFor(let event): return "Cannot make outcome for event: \(event)" + case .parseError(let error): return "\(error)" + case .error(let error): return "\(error)" + case .errorString(let string): return string + } + } +} diff --git a/OCKSample/Models/InstallationChannel.swift b/OCKSample/Models/InstallationChannel.swift new file mode 100644 index 00000000..24ea1fd3 --- /dev/null +++ b/OCKSample/Models/InstallationChannel.swift @@ -0,0 +1,13 @@ +// +// InstallationChannel.swift +// OCKSample +// +// Created by Corey Baker on 4/14/23. +// Copyright © 2023 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation + +enum InstallationChannel: String { + case global +} diff --git a/OCKSample/Models/MainViewPath.swift b/OCKSample/Models/MainViewPath.swift new file mode 100644 index 00000000..c3121345 --- /dev/null +++ b/OCKSample/Models/MainViewPath.swift @@ -0,0 +1,13 @@ +// +// MainViewPath.swift +// OCKSample +// +// Created by Corey Baker on 4/14/23. +// Copyright © 2023 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation + +enum MainViewPath { + case tabs +} diff --git a/OCKSample/Models/Installation.swift b/OCKSample/Models/Parse/Installation.swift similarity index 100% rename from OCKSample/Models/Installation.swift rename to OCKSample/Models/Parse/Installation.swift diff --git a/OCKSample/Models/User.swift b/OCKSample/Models/Parse/User.swift similarity index 100% rename from OCKSample/Models/User.swift rename to OCKSample/Models/Parse/User.swift diff --git a/OCKSample/Models/TaskID.swift b/OCKSample/Models/TaskID.swift new file mode 100644 index 00000000..192ff2aa --- /dev/null +++ b/OCKSample/Models/TaskID.swift @@ -0,0 +1,21 @@ +// +// TaskID.swift +// OCKSample +// +// Created by Corey Baker on 4/14/23. +// Copyright © 2023 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation + +enum TaskID { + static let doxylamine = "doxylamine" + static let nausea = "nausea" + static let stretch = "stretch" + static let kegels = "kegels" + static let steps = "steps" + + static var ordered: [String] { + [Self.steps, Self.doxylamine, Self.kegels, Self.stretch, Self.nausea] + } +} diff --git a/OCKSample/Models/UserType.swift b/OCKSample/Models/UserType.swift new file mode 100644 index 00000000..8e8b8ab8 --- /dev/null +++ b/OCKSample/Models/UserType.swift @@ -0,0 +1,20 @@ +// +// UserType.swift +// OCKSample +// +// Created by Corey Baker on 4/14/23. +// Copyright © 2023 Network Reconnaissance Lab. All rights reserved. +// + +import Foundation + +enum UserType: String, Codable { + case patient = "Patient" + case none = "None" + + // Return all types as an array, make sure to maintain order above + func allTypesAsArray() -> [String] { + return [UserType.patient.rawValue, + UserType.none.rawValue] + } +} diff --git a/OCKSample/OCKSampleApp.swift b/OCKSample/OCKSampleApp.swift index 9dc93976..85e5da94 100644 --- a/OCKSample/OCKSampleApp.swift +++ b/OCKSample/OCKSampleApp.swift @@ -21,7 +21,7 @@ struct OCKSampleApp: App { MainView() .environment(\.appDelegate, appDelegate) .accentColor(Color(tintColor)) - .careKitStyle(Styler()) + .careKitStyle(style) } } } diff --git a/OCKSample/Supporting Files/ParseCareKit.plist b/OCKSample/Supporting Files/ParseCareKit.plist index cfd4d064..08979cce 100644 --- a/OCKSample/Supporting Files/ParseCareKit.plist +++ b/OCKSample/Supporting Files/ParseCareKit.plist @@ -9,7 +9,7 @@ LiveQueryServer UseTransactions - + DeleteKeychainIfNeeded diff --git a/OCKSample/Utility.swift b/OCKSample/Utility.swift index 10708ae4..01faeaee 100644 --- a/OCKSample/Utility.swift +++ b/OCKSample/Utility.swift @@ -16,7 +16,7 @@ class Utility { // For classes, we can use "class" or "static" to declare type methods/properties. class func prepareSyncMessageForWatch() -> [String: Any] { var returnMessage = [String: Any]() - returnMessage[Constants.requestSync] = "new messages in Cloud" + returnMessage[Constants.requestSync] = "new messages on Remote" return returnMessage } @@ -48,17 +48,15 @@ class Utility { do { try await setDefaultACL() } catch { - Logger.login.error("Could not set defaultACL: \(error)") + Logger.utility.error("Could not set defaultACL: \(error)") } guard let appDelegate = AppDelegateKey.defaultValue else { - Logger.login.error("Could not setup remotes, AppDelegate is nil") + Logger.utility.error("Could not setup remotes, AppDelegate is nil") return } try await appDelegate.setupRemotes(uuid: remoteUUID) appDelegate.parseRemote.automaticallySynchronizes = true - - NotificationCenter.default.post(.init(name: Notification.Name(rawValue: Constants.requestSync))) return } @@ -106,7 +104,7 @@ class Utility { } } - class func createPreviewStoreManager() -> OCKSynchronizedStoreManager { + class func createPreviewStore() -> OCKStore { let store = OCKStore(name: Constants.noCareStoreName, type: .inMemory) let patientId = "preview" Task { @@ -114,14 +112,68 @@ class Utility { // If patient exists, assume store is already populated _ = try await store.fetchPatient(withID: patientId) } catch { - let patient = OCKPatient(id: patientId, + var patient = OCKPatient(id: patientId, givenName: "Preview", familyName: "Patient") + patient.birthday = Calendar.current.date(byAdding: .year, + value: -20, + to: Date()) _ = try? await store.addPatient(patient) try? await store.populateSampleData() } } - return .init(wrapping: store) + return store + } + + class func clearDeviceOnFirstRun(storeName: String? = nil) async { + // Clear items out of the Keychain on app first run. + if UserDefaults.standard.object(forKey: Constants.appName) == nil { + + if let storeName = storeName { + let store = OCKStore(name: storeName, type: .onDisk()) + do { + try store.delete() + } catch { + Logger.utility.error(""" + Could not delete OCKStore with name \"\(storeName)\" because of error: \(error) + """) + } + } else { + let localStore: OCKStore! + let parseStore: OCKStore! + + #if os(watchOS) + localStore = OCKStore(name: Constants.watchOSLocalCareStoreName, + type: .onDisk()) + parseStore = OCKStore(name: Constants.watchOSParseCareStoreName, + type: .onDisk()) + #else + localStore = OCKStore(name: Constants.iOSLocalCareStoreName, + type: .onDisk()) + parseStore = OCKStore(name: Constants.iOSParseCareStoreName, + type: .onDisk()) + #endif + + do { + try localStore.delete() + } catch { + Logger.utility.error("Could not delete local OCKStore because of error: \(error)") + } + do { + try parseStore.delete() + } catch { + Logger.utility.error("Could not delete parse OCKStore because of error: \(error)") + } + } + + // This is no longer the first run + UserDefaults.standard.setValue(String(Constants.appName), + forKey: Constants.appName) + UserDefaults.standard.synchronize() + if isSyncingWithRemote { + try? await User.logout() + } + } } #if os(iOS) @@ -134,7 +186,7 @@ class Utility { } return } - Logger.login.error("Error requesting HealthKit permissions: \(error)") + Logger.utility.error("Error requesting HealthKit permissions: \(error)") } } #endif diff --git a/OCKWatchSample Extension/AppDelegate.swift b/OCKWatchSample Extension/AppDelegate.swift index 0f870acb..066bb3bb 100644 --- a/OCKWatchSample Extension/AppDelegate.swift +++ b/OCKWatchSample Extension/AppDelegate.swift @@ -15,53 +15,64 @@ import WatchConnectivity import os.log class AppDelegate: NSObject, WKApplicationDelegate, ObservableObject { + // MARK: Public read private write properties - @Published private(set) var storeManager: OCKSynchronizedStoreManager! { + + @Published private(set) var store: OCKStore! { willSet { - StoreManagerKey.defaultValue = newValue - DispatchQueue.main.async { - NotificationCenter.default.post(.init(name: Notification.Name(rawValue: Constants.storeInitialized))) - self.objectWillChange.send() + newValue.synchronize { error in + let errorString = error?.localizedDescription ?? "Successful sync with remote!" + Logger.appDelegate.info("\(errorString)") } + self.objectWillChange.send() } } - private(set) var store: OCKStore! private(set) var parseRemote: ParseRemote! // MARK: Private read/write properties + private var sessionDelegate: SessionDelegate! private lazy var phoneRemote = OCKWatchConnectivityPeer() func applicationDidFinishLaunching() { Task { - do { - // Parse-server setup - try await PCKUtility.setupServer(fileName: Constants.parseConfigFileName) { _, completionHandler in - completionHandler(.performDefaultHandling, nil) - } + if isSyncingWithRemote { do { - _ = try await User.current() + // Parse-server setup + // swiftlint:disable:next line_length + try await PCKUtility.configureParse(fileName: Constants.parseConfigFileName) { _, completionHandler in + completionHandler(.performDefaultHandling, nil) + } + await Utility.clearDeviceOnFirstRun() do { - let uuid = try await Utility.getRemoteClockUUID() - try await self.setupRemotes(uuid: uuid) - parseRemote.automaticallySynchronizes = true - // swiftlint:disable:next line_length - NotificationCenter.default.post(.init(name: Notification.Name(rawValue: Constants.userLoggedIn))) - Logger.appDelegate.info("User is already signed in...") - store.synchronize { error in - let errorString = error?.localizedDescription ?? "Successful sync with remote!" - Logger.appDelegate.info("\(errorString)") + _ = try await User.current() + do { + let uuid = try await Utility.getRemoteClockUUID() + try await self.setupRemotes(uuid: uuid) + Logger.appDelegate.info("User is already signed in...") + } catch { + Logger.appDelegate.error("User is logged in, but missing remoteId: \(error)") + try await setupRemotes(uuid: nil) } + parseRemote.automaticallySynchronizes = true } catch { - Logger.appDelegate.error("User is logged in, but missing remoteId: \(error)") + Logger.appDelegate.info("User is not logged in...") try await setupRemotes(uuid: nil) } } catch { - Logger.appDelegate.info("User is not logged in...") - try await setupRemotes(uuid: nil) + Logger.appDelegate.info("Could not configure Parse Swift: \(error)") + } + } else { + await Utility.clearDeviceOnFirstRun() + do { + try await self.setupRemotes() + phoneRemote.automaticallySynchronizes = true + } catch { + Logger.appDelegate.error(""" + Could not populate + data stores: \(error) + """) } - } catch { - Logger.appDelegate.info("Could not configure Parse Swift: \(error)") } } } @@ -72,9 +83,10 @@ class AppDelegate: NSObject, WKApplicationDelegate, ObservableObject { } } + @MainActor func setupRemotes(uuid: UUID? = nil) async throws { do { - if isSyncingWithCloud { + if isSyncingWithRemote { if sessionDelegate == nil { sessionDelegate = RemoteSessionDelegate(store: store) WCSession.default.delegate = sessionDelegate @@ -86,19 +98,23 @@ class AppDelegate: NSObject, WKApplicationDelegate, ObservableObject { } parseRemote = try await ParseRemote(uuid: uuid, auto: false, - subscribeToServerUpdates: true) - store = OCKStore(name: Constants.watchOSParseCareStoreName, - remote: parseRemote) + subscribeToRemoteUpdates: true, + defaultACL: PCKUtility.getDefaultACL()) + let store = OCKStore(name: Constants.watchOSParseCareStoreName, + type: .onDisk(), + remote: parseRemote) parseRemote?.parseRemoteDelegate = self sessionDelegate.store = store - storeManager = OCKSynchronizedStoreManager(wrapping: store) + self.store = store } else { - store = OCKStore(name: Constants.watchOSLocalCareStoreName, - remote: phoneRemote) + let store = OCKStore(name: Constants.watchOSLocalCareStoreName, + type: .onDisk(), + remote: phoneRemote) phoneRemote.delegate = self - sessionDelegate = LocalSessionDelegate(remote: phoneRemote, store: store) + sessionDelegate = LocalSessionDelegate(remote: phoneRemote, + store: store) WCSession.default.delegate = sessionDelegate - storeManager = OCKSynchronizedStoreManager(wrapping: store) + self.store = store } WCSession.default.activate() } catch { @@ -107,6 +123,23 @@ class AppDelegate: NSObject, WKApplicationDelegate, ObservableObject { } } + @MainActor + func resetAppToInitialState() { + + do { + try self.store?.delete() + } catch { + Logger.appDelegate.error("Error deleting OCKStore: \(error)") + } + + parseRemote = nil + + let store = OCKStore(name: Constants.noCareStoreName, + type: .inMemory) + sessionDelegate.store = store + self.store = store + } + func applicationDidBecomeActive() { // swiftlint:disable:next line_length // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. diff --git a/OCKWatchSample Extension/Extensions/AppDelegate+ParseRemoteDelegate.swift b/OCKWatchSample Extension/Extensions/AppDelegate+ParseRemoteDelegate.swift index c3fb78d0..1c2a3796 100644 --- a/OCKWatchSample Extension/Extensions/AppDelegate+ParseRemoteDelegate.swift +++ b/OCKWatchSample Extension/Extensions/AppDelegate+ParseRemoteDelegate.swift @@ -20,37 +20,30 @@ extension AppDelegate: ParseRemoteDelegate { } } - func successfullyPushedDataToCloud() { + func successfullyPushedToRemote() { Logger.appDelegate.info("Finished pushing data") } + func provideStore() -> OCKAnyStoreProtocol { + guard let store = store else { + return OCKStore(name: Constants.noCareStoreName, type: .inMemory) + } + return store + } + func remote(_ remote: OCKRemoteSynchronizable, didUpdateProgress progress: Double) {} func chooseConflictResolution(conflicts: [OCKEntity], completion: @escaping OCKResultClosure) { // https://github.com/carekit-apple/CareKit/issues/567 - // Workaround to handle deleted and re-added outcomes. - // Always prefer updates over deletes. - let outcomes = conflicts.compactMap { conflict -> OCKOutcome? in - if case let .outcome(outcome) = conflict { - return outcome - } else { - return nil - } - } - - if outcomes.count == 2, - outcomes.contains(where: { $0.deletedDate != nil}), - let added = outcomes.first(where: { $0.deletedDate == nil}) { - - completion(.success(.outcome(added))) - return - } - - if let first = conflicts.first { - completion(.success(first)) - } else { - completion(.failure(.remoteSynchronizationFailed(reason: "Error, non selected for conflict"))) + // Last write wins + do { + let lastWrite = try conflicts + .max(by: { try $0.parseEntity().value.createdDate! > $1.parseEntity().value.createdDate! })! + + completion(.success(lastWrite)) + } catch { + completion(.failure(.invalidValue(reason: error.localizedDescription))) } } } diff --git a/OCKWatchSample Extension/Main/Care/CareView.swift b/OCKWatchSample Extension/Main/Care/CareView.swift index eefe870b..10bf8a0d 100644 --- a/OCKWatchSample Extension/Main/Care/CareView.swift +++ b/OCKWatchSample Extension/Main/Care/CareView.swift @@ -8,23 +8,27 @@ import CareKit import CareKitStore +import CareKitUI import SwiftUI import os.log struct CareView: View { - @EnvironmentObject private var appDelegate: AppDelegate - @StateObject var viewModel = CareViewModel() + private static var query: OCKEventQuery { + var query = OCKEventQuery(for: Date()) + query.taskIDs = [TaskID.stretch, TaskID.kegels] + return query + } + @CareStoreFetchRequest(query: query) private var events var body: some View { ScrollView { - SimpleTaskView(taskID: TaskID.kegels, - eventQuery: .init(for: Date()), - storeManager: appDelegate.storeManager) - InstructionsTaskView(taskID: TaskID.stretch, - eventQuery: .init(for: Date()), - storeManager: appDelegate.storeManager) - }.onReceive(appDelegate.$storeManager) { newStoreManager in - viewModel.synchronizeStore(storeManager: newStoreManager) + ForEach(events) { event in + if event.result.task.id == TaskID.kegels { + SimpleTaskView(event: event) + } else if event.result.task.id == TaskID.stretch { + InstructionsTaskView(event: event) + } + } } } } @@ -33,5 +37,6 @@ struct ContentView_Previews: PreviewProvider { static var previews: some View { CareView() .accentColor(Color(TintColorKey.defaultValue)) + .environment(\.careStore, Utility.createPreviewStore()) } } diff --git a/OCKWatchSample Extension/Main/Care/CareViewModel.swift b/OCKWatchSample Extension/Main/Care/CareViewModel.swift deleted file mode 100644 index c8d84457..00000000 --- a/OCKWatchSample Extension/Main/Care/CareViewModel.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// CareViewModel.swift -// OCKWatchSample Extension -// -// Created by Corey Baker on 1/5/22. -// Copyright © 2022 Network Reconnaissance Lab. All rights reserved. -// - -import Foundation -import Combine -import CareKit -import CareKitStore -import WatchConnectivity -import os.log - -class CareViewModel: ObservableObject { - - func synchronizeStore(storeManager: OCKSynchronizedStoreManager?) { - guard let store = storeManager?.store as? OCKStore else { - Logger.feed.info("Could not cast to OCKStore") - return - } - store.synchronize { error in - let errorString = error?.localizedDescription ?? "Successful sync with remote!" - Logger.feed.info("\(errorString)") - } - } -} diff --git a/OCKWatchSample Extension/Main/Login/LoginView.swift b/OCKWatchSample Extension/Main/Login/LoginView.swift index 131c57f3..75d1a9bd 100644 --- a/OCKWatchSample Extension/Main/Login/LoginView.swift +++ b/OCKWatchSample Extension/Main/Login/LoginView.swift @@ -9,7 +9,6 @@ import SwiftUI struct LoginView: View { - @EnvironmentObject private var appDelegate: AppDelegate @ObservedObject var viewModel: LoginViewModel var body: some View { diff --git a/OCKWatchSample Extension/Main/Login/LoginViewModel.swift b/OCKWatchSample Extension/Main/Login/LoginViewModel.swift index 9bef7ef9..1fc74101 100644 --- a/OCKWatchSample Extension/Main/Login/LoginViewModel.swift +++ b/OCKWatchSample Extension/Main/Login/LoginViewModel.swift @@ -16,24 +16,15 @@ class LoginViewModel: ObservableObject { @Published private(set) var isLoggedOut = true init() { - NotificationCenter.default.addObserver(self, - selector: #selector(userLoggedIn(_:)), - name: Notification.Name(rawValue: Constants.userLoggedIn), - object: nil) Task { await self.checkStatus() } } // MARK: Helpers (private) - @objc private func userLoggedIn(_ notification: Notification) { - Task { - await self.checkStatus() - } - } @MainActor - private func checkStatus() async { + func checkStatus() async { let isLoggedOut = self.isLoggedOut do { _ = try await User.current() @@ -59,15 +50,6 @@ class LoginViewModel: ObservableObject { let user = try await User.become(sessionToken: sessionToken) Logger.login.info("Parse login successful \(user, privacy: .private)") try await Utility.setupRemoteAfterLogin() - guard let watchDelegate = AppDelegateKey.defaultValue else { - Logger.login.error("ApplicationDelegate should not be nil") - return - } - watchDelegate.store.synchronize { error in - NotificationCenter.default.post(.init(name: Notification.Name(rawValue: Constants.userLoggedIn))) - let errorString = error?.localizedDescription ?? "Successful sync with remote!" - Logger.watch.info("\(errorString)") - } // Setup installation to receive push notifications Task { diff --git a/OCKWatchSample Extension/Main/MainView.swift b/OCKWatchSample Extension/Main/MainView.swift index f598ab72..740930f5 100644 --- a/OCKWatchSample Extension/Main/MainView.swift +++ b/OCKWatchSample Extension/Main/MainView.swift @@ -7,10 +7,15 @@ // import SwiftUI +import CareKitStore +import os.log struct MainView: View { - @StateObject var loginViewModel = LoginViewModel() - @State var path = [MainViewPath]() + @EnvironmentObject private var appDelegate: AppDelegate + @StateObject private var loginViewModel = LoginViewModel() + @State private var path = [MainViewPath]() + @State private var store = OCKStore(name: Constants.noCareStoreName, + type: .inMemory) var body: some View { NavigationStack(path: $path) { @@ -19,11 +24,12 @@ struct MainView: View { switch destination { case .tabs: CareView() + .navigationBarHidden(true) } } .navigationBarHidden(true) .onAppear { - guard isSyncingWithCloud else { + guard isSyncingWithRemote else { path = [.tabs] return } @@ -34,13 +40,33 @@ struct MainView: View { path = [.tabs] } } + .environment(\.careStore, store) .onReceive(loginViewModel.$isLoggedOut, perform: { isLoggedOut in + guard isSyncingWithRemote else { + path = [.tabs] + return + } guard !isLoggedOut else { path = [] return } path = [.tabs] }) + .onReceive(appDelegate.$store) { newStore in + Task { + await loginViewModel.checkStatus() + } + guard let newStore = newStore, + store.name != newStore.name else { + return + } + store = newStore + guard isSyncingWithRemote else { + path = [.tabs] + return + } + path = [.tabs] + } } } diff --git a/OCKWatchSample Extension/OCKWatchSampleApp.swift b/OCKWatchSample Extension/OCKWatchSampleApp.swift index c882104f..a829d80a 100644 --- a/OCKWatchSample Extension/OCKWatchSampleApp.swift +++ b/OCKWatchSample Extension/OCKWatchSampleApp.swift @@ -11,15 +11,13 @@ import SwiftUI @main struct OCKWatchSampleApp: App { - @WKApplicationDelegateAdaptor private var delegate: AppDelegate - @Environment(\.scenePhase) private var scenePhase + @WKApplicationDelegateAdaptor private var appDelegate: AppDelegate @Environment(\.tintColor) private var tintColor @Environment(\.customStyler) private var style - @State var isActive = false @SceneBuilder var body: some Scene { WindowGroup { MainView() - .environment(\.appDelegate, delegate) + .environment(\.appDelegate, appDelegate) .accentColor(Color(tintColor)) .careKitStyle(style) } diff --git a/README.md b/README.md index a6d4b501..53d09ee3 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # 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) +![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 3.0+](https://img.shields.io/badge/CareKit-3.0%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). -**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.** +**Similar to the [What's New in CareKit](https://developer.apple.com/videos/play/wwdc2020/10151/) WWDC20 video, this app syncs between data between iOS and an Apple Watch (setting the flag `isSyncingWithRemote` in `Constants.swift` to `isSyncingWithRemote = false`. Different from the video, setting `isSyncingWithRemote = true` (default behavior) in the aforementioned file syncs iOS and watchOS to a Parse Server.** ParseCareKit synchronizes the following entities to Parse tables/classes using [Parse-Swift](https://github.com/netreconlab/Parse-Swift):