diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 1591d59..9c2063e 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -23,25 +23,4 @@ jobs: name: Markdown Link Check uses: StanfordBDHG/.github/.github/workflows/markdown-link-check.yml@v2 permissions: - contents: read - buildandtest: - name: Build and Test - uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 - permissions: - contents: read - with: - artifactname: StrokeCog.xcresult - runsonlabels: '["macOS", "self-hosted"]' - setupSimulators: true - setupfirebaseemulator: true - customcommand: "firebase emulators:exec 'fastlane test'" - uploadcoveragereport: - name: Upload Coverage Report - needs: buildandtest - uses: StanfordBDHG/.github/.github/workflows/create-and-upload-coverage-report.yml@v2 - permissions: - contents: read - with: - coveragereports: StrokeCog.xcresult - secrets: - token: ${{ secrets.CODECOV_TOKEN }} + contents: read \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml index b6c56af..f26caad 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -292,8 +292,6 @@ only_rules: - switch_case_alignment # Shorthand syntactic sugar should be used, i.e. [Int] instead of Array. - syntactic_sugar - # TODOs and FIXMEs should be resolved. - - todo # Prefer someBool.toggle() over someBool = !someBool. - toggle_bool # Trailing closure syntax should be used whenever possible. diff --git a/StrokeCog.xcodeproj/project.pbxproj b/StrokeCog.xcodeproj/project.pbxproj index 20c689d..a7866ec 100644 --- a/StrokeCog.xcodeproj/project.pbxproj +++ b/StrokeCog.xcodeproj/project.pbxproj @@ -39,7 +39,6 @@ 2FE5DC4129EDD7EE004B9AB4 /* StorageKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3F29EDD7EE004B9AB4 /* StorageKeys.swift */; }; 2FE5DC4529EDD7F2004B9AB4 /* Binding+Negate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4229EDD7F2004B9AB4 /* Binding+Negate.swift */; }; 2FE5DC4629EDD7F2004B9AB4 /* Bundle+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4329EDD7F2004B9AB4 /* Bundle+Image.swift */; }; - 2FE5DC4729EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4429EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift */; }; 2FE5DC4E29EDD7FA004B9AB4 /* ScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4829EDD7FA004B9AB4 /* ScheduleView.swift */; }; 2FE5DC4F29EDD7FA004B9AB4 /* EventContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4929EDD7FA004B9AB4 /* EventContext.swift */; }; 2FE5DC5029EDD7FA004B9AB4 /* EventContextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4A29EDD7FA004B9AB4 /* EventContextView.swift */; }; @@ -67,8 +66,17 @@ 5680DD392AB8983D004E6D4A /* PackageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5680DD382AB8983D004E6D4A /* PackageCell.swift */; }; 5680DD3E2AB8CD84004E6D4A /* ContributionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5680DD3D2AB8CD84004E6D4A /* ContributionsTest.swift */; }; 56F6F2A02AB441930022FE5A /* ContributionsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F6F29F2AB441930022FE5A /* ContributionsList.swift */; }; + 6347EB642BBBA895008E0C4A /* StrokeCogMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6347EB632BBBA895008E0C4A /* StrokeCogMapView.swift */; }; + 6347EB6A2BBBF3AC008E0C4A /* MapboxMaps in Frameworks */ = {isa = PBXBuildFile; productRef = 6347EB692BBBF3AC008E0C4A /* MapboxMaps */; }; + 6347EB742BBBF442008E0C4A /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6347EB732BBBF442008E0C4A /* Constants.swift */; }; + 63497B702BBF6ECE001F8419 /* LocationDataPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63497B6F2BBF6ECE001F8419 /* LocationDataPoint.swift */; }; 63BBF8162BB8993B006890CE /* StudyIDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63BBF8152BB8993B006890CE /* StudyIDView.swift */; }; 63BBF8192BB89CF7006890CE /* studyIDs.csv in Resources */ = {isa = PBXBuildFile; fileRef = 63BBF8182BB89CF7006890CE /* studyIDs.csv */; }; + 63F4C3992BBCCC300033D985 /* MapboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F4C3982BBCCC300033D985 /* MapboxView.swift */; }; + 63F4C39B2BBCCCF80033D985 /* LocationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F4C39A2BBCCCF80033D985 /* LocationModule.swift */; }; + 63F4C39D2BBCCD200033D985 /* LocationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F4C39C2BBCCD200033D985 /* LocationUtils.swift */; }; + 63F4C39F2BBCCDB70033D985 /* Date+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F4C39E2BBCCDB70033D985 /* Date+Helpers.swift */; }; + 63F4C3A32BBCE79B0033D985 /* LocationPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F4C3A22BBCE79B0033D985 /* LocationPermissions.swift */; }; 653A2551283387FE005D4D48 /* StrokeCog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A2550283387FE005D4D48 /* StrokeCog.swift */; }; 653A255528338800005D4D48 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 653A255428338800005D4D48 /* Assets.xcassets */; }; 653A256228338800005D4D48 /* StrokeCogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A256128338800005D4D48 /* StrokeCogTests.swift */; }; @@ -127,7 +135,6 @@ 2FE5DC3F29EDD7EE004B9AB4 /* StorageKeys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageKeys.swift; sourceTree = ""; }; 2FE5DC4229EDD7F2004B9AB4 /* Binding+Negate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Binding+Negate.swift"; sourceTree = ""; }; 2FE5DC4329EDD7F2004B9AB4 /* Bundle+Image.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+Image.swift"; sourceTree = ""; }; - 2FE5DC4429EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CodableArray+RawRepresentable.swift"; sourceTree = ""; }; 2FE5DC4829EDD7FA004B9AB4 /* ScheduleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScheduleView.swift; sourceTree = ""; }; 2FE5DC4929EDD7FA004B9AB4 /* EventContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventContext.swift; sourceTree = ""; }; 2FE5DC4A29EDD7FA004B9AB4 /* EventContextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventContextView.swift; sourceTree = ""; }; @@ -142,8 +149,16 @@ 5680DD382AB8983D004E6D4A /* PackageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageCell.swift; sourceTree = ""; }; 5680DD3D2AB8CD84004E6D4A /* ContributionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContributionsTest.swift; sourceTree = ""; }; 56F6F29F2AB441930022FE5A /* ContributionsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContributionsList.swift; sourceTree = ""; }; + 6347EB632BBBA895008E0C4A /* StrokeCogMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeCogMapView.swift; sourceTree = ""; }; + 6347EB732BBBF442008E0C4A /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + 63497B6F2BBF6ECE001F8419 /* LocationDataPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationDataPoint.swift; sourceTree = ""; }; 63BBF8152BB8993B006890CE /* StudyIDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StudyIDView.swift; sourceTree = ""; }; 63BBF8182BB89CF7006890CE /* studyIDs.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = studyIDs.csv; sourceTree = ""; }; + 63F4C3982BBCCC300033D985 /* MapboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapboxView.swift; sourceTree = ""; }; + 63F4C39A2BBCCCF80033D985 /* LocationModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationModule.swift; sourceTree = ""; }; + 63F4C39C2BBCCD200033D985 /* LocationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationUtils.swift; sourceTree = ""; }; + 63F4C39E2BBCCDB70033D985 /* Date+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Helpers.swift"; sourceTree = ""; }; + 63F4C3A22BBCE79B0033D985 /* LocationPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissions.swift; sourceTree = ""; }; 653A254D283387FE005D4D48 /* StrokeCog.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StrokeCog.app; sourceTree = BUILT_PRODUCTS_DIR; }; 653A2550283387FE005D4D48 /* StrokeCog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeCog.swift; sourceTree = ""; }; 653A255428338800005D4D48 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -166,6 +181,7 @@ 2FE5DC6429EDD883004B9AB4 /* SpeziAccount in Frameworks */, 2FB099AF2A875DF100B20952 /* FirebaseAuth in Frameworks */, 97D73D6A2AD860AD00B47FA0 /* SpeziFirebaseStorage in Frameworks */, + 6347EB6A2BBBF3AC008E0C4A /* MapboxMaps in Frameworks */, 2FE5DC6729EDD894004B9AB4 /* SpeziContact in Frameworks */, 2FE5DC8429EDD934004B9AB4 /* SpeziQuestionnaire in Frameworks */, 2FB099B32A875DF100B20952 /* FirebaseFirestoreSwift in Frameworks */, @@ -246,6 +262,7 @@ 2FE5DC3029EDD7CA004B9AB4 /* HealthKitPermissions.swift */, 2F65B44D2A3B8B0600A36932 /* NotificationPermissions.swift */, 63BBF8152BB8993B006890CE /* StudyIDView.swift */, + 63F4C3A22BBCE79B0033D985 /* LocationPermissions.swift */, ); path = Onboarding; sourceTree = ""; @@ -282,6 +299,7 @@ children = ( 2FE5DC3E29EDD7ED004B9AB4 /* FeatureFlags.swift */, 2FE5DC3F29EDD7EE004B9AB4 /* StorageKeys.swift */, + 6347EB732BBBF442008E0C4A /* Constants.swift */, ); path = SharedContext; sourceTree = ""; @@ -291,7 +309,7 @@ children = ( 2FE5DC4229EDD7F2004B9AB4 /* Binding+Negate.swift */, 2FE5DC4329EDD7F2004B9AB4 /* Bundle+Image.swift */, - 2FE5DC4429EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift */, + 63F4C39E2BBCCDB70033D985 /* Date+Helpers.swift */, ); path = Helper; sourceTree = ""; @@ -307,6 +325,25 @@ path = Contributions; sourceTree = ""; }; + 6347EB622BBBA874008E0C4A /* Map */ = { + isa = PBXGroup; + children = ( + 6347EB632BBBA895008E0C4A /* StrokeCogMapView.swift */, + 63F4C3982BBCCC300033D985 /* MapboxView.swift */, + ); + path = Map; + sourceTree = ""; + }; + 637AA5CF2BBDA686007BD7A3 /* Location */ = { + isa = PBXGroup; + children = ( + 63F4C39A2BBCCCF80033D985 /* LocationModule.swift */, + 63F4C39C2BBCCD200033D985 /* LocationUtils.swift */, + 63497B6F2BBF6ECE001F8419 /* LocationDataPoint.swift */, + ); + path = Location; + sourceTree = ""; + }; 653A2544283387FE005D4D48 = { isa = PBXGroup; children = ( @@ -342,6 +379,8 @@ 2FE5DC3B29EDD7D0004B9AB4 /* Schedule */, 2FE5DC2729EDD38D004B9AB4 /* Contacts */, 56F6F29E2AB441640022FE5A /* Contributions */, + 637AA5CF2BBDA686007BD7A3 /* Location */, + 6347EB622BBBA874008E0C4A /* Map */, 2F4FC8D529EE69BE00BFFE26 /* MockUpload */, 2FE5DC3C29EDD7DA004B9AB4 /* SharedContext */, 2FE5DC3D29EDD7E4004B9AB4 /* Helper */, @@ -431,6 +470,7 @@ 97D73D692AD860AD00B47FA0 /* SpeziFirebaseStorage */, A9D83F952B083794000D0C78 /* SpeziFirebaseAccountStorage */, A92E4DEF2BAA001100AC8DE8 /* OrderedCollections */, + 6347EB692BBBF3AC008E0C4A /* MapboxMaps */, ); productName = StrokeCog; productReference = 653A254D283387FE005D4D48 /* StrokeCog.app */; @@ -526,6 +566,7 @@ 2FB099B42A875E2B00B20952 /* XCRemoteSwiftPackageReference "HealthKitOnFHIR" */, 5661551B2AB8384200209B80 /* XCRemoteSwiftPackageReference "swift-package-list" */, A92E4DEE2BAA001100AC8DE8 /* XCRemoteSwiftPackageReference "swift-collections" */, + 6347EB662BBBF34A008E0C4A /* XCRemoteSwiftPackageReference "mapbox-maps-ios" */, ); productRefGroup = 653A254E283387FE005D4D48 /* Products */; projectDirPath = ""; @@ -608,13 +649,20 @@ 2FE5DC3729EDD7CA004B9AB4 /* OnboardingFlow.swift in Sources */, 2F1AC9DF2B4E840E00C24973 /* StrokeCog.docc in Sources */, 2FF53D8D2A8729D600042B76 /* StrokeCogStandard.swift in Sources */, - 2FE5DC4729EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift in Sources */, + 63497B702BBF6ECE001F8419 /* LocationDataPoint.swift in Sources */, A9720E432ABB68CC00872D23 /* AccountSetupHeader.swift in Sources */, 2FE5DC4029EDD7EE004B9AB4 /* FeatureFlags.swift in Sources */, 2FE5DC4629EDD7F2004B9AB4 /* Bundle+Image.swift in Sources */, 2FE5DC4F29EDD7FA004B9AB4 /* EventContext.swift in Sources */, + 6347EB642BBBA895008E0C4A /* StrokeCogMapView.swift in Sources */, + 63F4C3992BBCCC300033D985 /* MapboxView.swift in Sources */, 2FE5DC5029EDD7FA004B9AB4 /* EventContextView.swift in Sources */, + 63F4C3A32BBCE79B0033D985 /* LocationPermissions.swift in Sources */, + 63F4C39F2BBCCDB70033D985 /* Date+Helpers.swift in Sources */, 2F4E23832989D51F0013F3D9 /* StrokeCogTestingSetup.swift in Sources */, + 63F4C39B2BBCCCF80033D985 /* LocationModule.swift in Sources */, + 6347EB742BBBF442008E0C4A /* Constants.swift in Sources */, + 63F4C39D2BBCCD200033D985 /* LocationUtils.swift in Sources */, 2FE5DC5329EDD7FA004B9AB4 /* Bundle+Questionnaire.swift in Sources */, 2FE5DC5129EDD7FA004B9AB4 /* StrokeCogTaskContext.swift in Sources */, 56F6F2A02AB441930022FE5A /* ContributionsList.swift in Sources */, @@ -753,8 +801,9 @@ INFOPLIST_KEY_NSCameraUsageDescription = "This message should never appear. Please adjust this when you start using camera information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; INFOPLIST_KEY_NSHealthShareUsageDescription = "The StrokeCog uses the step count to demonstrate Spezi's integration with HealthKit."; INFOPLIST_KEY_NSHealthUpdateUsageDescription = "The StrokeCog uses the step count to demonstrate Spezi's integration with HealthKit."; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "StrokeCog tracks your location for a study."; + INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "StrokeCog tracks your location for a study."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "StrokeCog tracks your location for a study."; INFOPLIST_KEY_NSMicrophoneUsageDescription = "This message should never appear. Please adjust this when you start using microphone information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; INFOPLIST_KEY_NSMotionUsageDescription = "This message should never appear. Please adjust this when you start using motion information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "This message should never appear. Please adjust this when you start using speecg information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; @@ -955,8 +1004,9 @@ INFOPLIST_KEY_NSCameraUsageDescription = "This message should never appear. Please adjust this when you start using camera information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; INFOPLIST_KEY_NSHealthShareUsageDescription = "The StrokeCog uses the step count to demonstrate Spezi's integration with HealthKit."; INFOPLIST_KEY_NSHealthUpdateUsageDescription = "The StrokeCog uses the step count to demonstrate Spezi's integration with HealthKit."; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "StrokeCog tracks your location for a study."; + INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "StrokeCog tracks your location for a study."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "StrokeCog tracks your location for a study."; INFOPLIST_KEY_NSMicrophoneUsageDescription = "This message should never appear. Please adjust this when you start using microphone information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; INFOPLIST_KEY_NSMotionUsageDescription = "This message should never appear. Please adjust this when you start using motion information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "This message should never appear. Please adjust this when you start using speecg information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; @@ -1002,8 +1052,9 @@ INFOPLIST_KEY_NSCameraUsageDescription = "This message should never appear. Please adjust this when you start using camera information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; INFOPLIST_KEY_NSHealthShareUsageDescription = "The StrokeCog uses the step count to demonstrate Spezi's integration with HealthKit."; INFOPLIST_KEY_NSHealthUpdateUsageDescription = "The StrokeCog uses the step count to demonstrate Spezi's integration with HealthKit."; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "StrokeCog tracks your location for a study."; + INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "StrokeCog tracks your location for a study."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "StrokeCog tracks your location for a study."; INFOPLIST_KEY_NSMicrophoneUsageDescription = "This message should never appear. Please adjust this when you start using microphone information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; INFOPLIST_KEY_NSMotionUsageDescription = "This message should never appear. Please adjust this when you start using motion information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "This message should never appear. Please adjust this when you start using speecg information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; @@ -1271,6 +1322,14 @@ minimumVersion = 3.0.10; }; }; + 6347EB662BBBF34A008E0C4A /* XCRemoteSwiftPackageReference "mapbox-maps-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/mapbox/mapbox-maps-ios.git"; + requirement = { + branch = main; + kind = branch; + }; + }; 97F466E62A76BBEE005DC9B4 /* XCRemoteSwiftPackageReference "SpeziOnboarding" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziOnboarding"; @@ -1404,6 +1463,11 @@ package = 5661551B2AB8384200209B80 /* XCRemoteSwiftPackageReference "swift-package-list" */; productName = "plugin:SwiftPackageListJSONPlugin"; }; + 6347EB692BBBF3AC008E0C4A /* MapboxMaps */ = { + isa = XCSwiftPackageProductDependency; + package = 6347EB662BBBF34A008E0C4A /* XCRemoteSwiftPackageReference "mapbox-maps-ios" */; + productName = MapboxMaps; + }; 9739A0C52AD7B5730084BEA5 /* FirebaseStorage */ = { isa = XCSwiftPackageProductDependency; package = 2FE5DC9029EDD9C3004B9AB4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; diff --git a/StrokeCog.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/StrokeCog.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d592ae6..7ca7f03 100644 --- a/StrokeCog.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/StrokeCog.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "9ddbf125e828f8f258514b9b7b616777823852827ae6b79035a44b66b986b5e0", + "originHash" : "743bf7cd89ddbade5c78d5f9c6554803b47ff2ebf7a758e70d2c9c4b9167c70f", "pins" : [ { "identity" : "abseil-cpp-binary", @@ -109,6 +109,33 @@ "version" : "1.22.4" } }, + { + "identity" : "mapbox-common-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mapbox/mapbox-common-ios.git", + "state" : { + "revision" : "2af5c688a58d8019af1376a0a76f505a5f36a27b", + "version" : "24.3.0-rc.1" + } + }, + { + "identity" : "mapbox-core-maps-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mapbox/mapbox-core-maps-ios.git", + "state" : { + "revision" : "a85b77f312ecd430245c618d107050fa42d5afc2", + "version" : "11.3.0-rc.1" + } + }, + { + "identity" : "mapbox-maps-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mapbox/mapbox-maps-ios.git", + "state" : { + "branch" : "main", + "revision" : "9d1d35636f1cbbb9e0027ba205f32f12c75820b2" + } + }, { "identity" : "nanopb", "kind" : "remoteSourceControl", @@ -289,6 +316,15 @@ "version" : "1.26.0" } }, + { + "identity" : "turf-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mapbox/turf-swift.git", + "state" : { + "revision" : "213050191cfcb3d5aa76e1fa90c6ff1e182a42ca", + "version" : "2.8.0" + } + }, { "identity" : "xctestextensions", "kind" : "remoteSourceControl", diff --git a/StrokeCog/Helper/CodableArray+RawRepresentable.swift b/StrokeCog/Helper/CodableArray+RawRepresentable.swift deleted file mode 100644 index 61da25b..0000000 --- a/StrokeCog/Helper/CodableArray+RawRepresentable.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// This source file is part of the StrokeCog based on the Stanford Spezi Template Application project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import Foundation - - -extension Array: RawRepresentable where Element: Codable { - public var rawValue: String { - guard let data = try? JSONEncoder().encode(self), - let rawValue = String(data: data, encoding: .utf8) else { - return "[]" - } - return rawValue - } - - public init?(rawValue: String) { - guard let data = rawValue.data(using: .utf8), - let result = try? JSONDecoder().decode([Element].self, from: data) else { - return nil - } - self = result - } -} diff --git a/StrokeCog/Helper/Date+Helpers.swift b/StrokeCog/Helper/Date+Helpers.swift new file mode 100644 index 0000000..b95c17b --- /dev/null +++ b/StrokeCog/Helper/Date+Helpers.swift @@ -0,0 +1,14 @@ +// +// Date+Helpers.swift +// StrokeCog +// +// Created by Vishnu Ravi on 4/2/24. + +import Foundation + +extension Date { + /// Returns the start of the day for the Date + var startOfDay: Date { + Calendar.current.startOfDay(for: self) + } +} diff --git a/StrokeCog/Home.swift b/StrokeCog/Home.swift index 1058049..5b67296 100644 --- a/StrokeCog/Home.swift +++ b/StrokeCog/Home.swift @@ -16,6 +16,7 @@ struct HomeView: View { case schedule case contact case mockUpload + case map } static var accountEnabled: Bool { @@ -29,6 +30,11 @@ struct HomeView: View { var body: some View { TabView(selection: $selectedTab) { + StrokeCogMapView() + .tag(Tabs.map) + .tabItem { + Label("Map", systemImage: "map.circle") + } ScheduleView(presentingAccount: $presentingAccount) .tag(Tabs.schedule) .tabItem { diff --git a/StrokeCog/Location/LocationDataPoint.swift b/StrokeCog/Location/LocationDataPoint.swift new file mode 100644 index 0000000..d51faea --- /dev/null +++ b/StrokeCog/Location/LocationDataPoint.swift @@ -0,0 +1,18 @@ +// +// LocationDataPoint.swift +// StrokeCog +// +// Created by Vishnu Ravi on 4/4/24. +// + +import CoreLocation +import Foundation + +struct LocationDataPoint: Codable { + var currentDate: Date + var time: TimeInterval + var latitude: CLLocationDegrees + var longitude: CLLocationDegrees + var studyID: String = "" + var updatedBy: String = "" +} diff --git a/StrokeCog/Location/LocationModule.swift b/StrokeCog/Location/LocationModule.swift new file mode 100644 index 0000000..bb17f46 --- /dev/null +++ b/StrokeCog/Location/LocationModule.swift @@ -0,0 +1,136 @@ +// +// LocationService.swift +// LifeSpace +// +// Created by Vishnu Ravi on 4/2/24. + +import CoreLocation +import Firebase +import Foundation +import Spezi + +public class LocationModule: NSObject, CLLocationManagerDelegate, Module, DefaultInitializable, EnvironmentAccessible { + @Dependency private var standard: StrokeCogStandard? + + private(set) var manager = CLLocationManager() + public var allLocations = [CLLocationCoordinate2D]() + public var onLocationsUpdated: (([CLLocationCoordinate2D]) -> Void)? + + private var previousLocation: CLLocationCoordinate2D? + private var previousDate: Date? + + @Published var authorizationStatus: CLAuthorizationStatus = CLLocationManager().authorizationStatus + @Published var canShowRequestMessage = true + + private var lastKnownLocation: CLLocationCoordinate2D? { + didSet { + guard let lastKnownLocation = lastKnownLocation else { + return + } + self.appendNewLocationPoint(point: lastKnownLocation) + } + } + + override public required init() { + super.init() + manager.delegate = self + + // If user doesn't have a tracking preference, default to true + if UserDefaults.standard.value(forKey: Constants.prefTrackingStatus) == nil { + UserDefaults.standard.set(true, forKey: Constants.prefTrackingStatus) + } + + // If tracking status is true, start tracking + if UserDefaults.standard.bool(forKey: Constants.prefTrackingStatus) { + self.startTracking() + } + + // Disable Mapbox telemetry + UserDefaults.standard.set(false, forKey: "MGLMapboxMetricsEnabled") + } + + public func startTracking() { + if CLLocationManager.locationServicesEnabled() { + self.manager.startUpdatingLocation() + self.manager.startMonitoringSignificantLocationChanges() + self.manager.allowsBackgroundLocationUpdates = true + self.manager.pausesLocationUpdatesAutomatically = false + self.manager.showsBackgroundLocationIndicator = false + print("[LIFESPACE] Starting tracking...") + } else { + print("[LIFESPACE] Cannot start tracking - location services are not enabled.") + } + } + + public func stopTracking() { + self.manager.stopUpdatingLocation() + self.manager.stopMonitoringSignificantLocationChanges() + print("[LIFESPACE] Stopping tracking...") + } + + public func requestAuthorizationLocation() { + self.manager.requestWhenInUseAuthorization() + self.manager.requestAlwaysAuthorization() + } + + /// Get all the points for a particular date from the database + /// - Parameter date: the date for which to fetch all points + func fetchPoints(date: Date = Date()) { + // TODO: Fetch from Firestore + } + + /// Adds a new point to the map and saves the location to the database, + /// if it meets the criteria to be added. + /// - Parameter point: the point to add + private func appendNewLocationPoint(point: CLLocationCoordinate2D) { + var add = true + + if let previousLocation = previousLocation, + let previousDate = previousDate { + // Check if distance between current point and previous point is greater than the minimum + add = LocationUtils.isAboveMinimumDistance( + previousLocation: previousLocation, + currentLocation: point + ) + + // Reset all points when day changes + if Date().startOfDay != previousDate.startOfDay { + add = true + fetchPoints() + } + } + + if add { + // update local location data for map + allLocations.append(point) + onLocationsUpdated?(allLocations) + previousLocation = point + previousDate = Date() + + Task { + do { + try await standard?.add(location: point) + } catch { + print(error.localizedDescription) + } + } + } + } + + public func userAuthorizeAlways() -> Bool { + self.manager.authorizationStatus == .authorizedAlways + } + + public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + // An additional check that we only append points if location tracking is turned on + guard UserDefaults.standard.bool(forKey: Constants.prefTrackingStatus) else { + return + } + + lastKnownLocation = locations.first?.coordinate + } + + public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + authorizationStatus = manager.authorizationStatus + } +} diff --git a/StrokeCog/Location/LocationUtils.swift b/StrokeCog/Location/LocationUtils.swift new file mode 100644 index 0000000..a66da81 --- /dev/null +++ b/StrokeCog/Location/LocationUtils.swift @@ -0,0 +1,33 @@ +// +// LocationUtils.swift +// LifeSpace +// +// Created by Vishnu Ravi on 7/12/22. +// Copyright © 2022 LifeSpace. All rights reserved. +// + +import CoreLocation +import Foundation + +enum LocationUtils { + /// Checks if the last two points are above the minimum distance apart required to record them for the study. + /// - Parameters: + /// - previousLocation: the last point recorded + /// - currentLocation: the latest point + /// - Returns: Boolean + static func isAboveMinimumDistance( + previousLocation: CLLocationCoordinate2D, + currentLocation: CLLocationCoordinate2D + ) -> Bool { + let lastLocation = CLLocation( + latitude: previousLocation.latitude, + longitude: previousLocation.longitude + ) + let newLocation = CLLocation( + latitude: currentLocation.latitude, + longitude: currentLocation.longitude + ) + let distanceInMeters = newLocation.distance(from: lastLocation) + return distanceInMeters > Constants.minDistanceBetweenPoints + } +} diff --git a/StrokeCog/Map/MapboxView.swift b/StrokeCog/Map/MapboxView.swift new file mode 100644 index 0000000..5855f50 --- /dev/null +++ b/StrokeCog/Map/MapboxView.swift @@ -0,0 +1,120 @@ +// +// MapboxView.swift +// StrokeCog +// +// Created by Vishnu Ravi on 4/2/24. +// + +import MapboxMaps +import Spezi +import SwiftUI + +struct MapManagerViewWrapper: UIViewControllerRepresentable { + typealias UIViewControllerType = MapManagerView + + @Environment(LocationModule.self) private var locationModule + + func makeUIViewController(context: Context) -> MapManagerView { + MapManagerView(locationModule: locationModule) + } + + func updateUIViewController(_ uiViewController: MapManagerView, context: Context) {} +} + +public class MapManagerView: UIViewController { + private enum Constants { + static let geoSourceId = "GEOSOURCE" + static let circleLayerId = "CIRCLELAYER" + static let zoomLevel: Double = 14.0 + static let countryLabelLayerId = "country-label" + } + + private var locationModule: LocationModule? + + private lazy var mapView: MapView = { + let map = MapView(frame: view.bounds) + map.autoresizingMask = [.flexibleWidth, .flexibleHeight] + map.location.options.puckType = .puck2D() + map.ornaments.options.scaleBar.visibility = .visible + return map + }() + + convenience init() { + self.init(locationModule: nil) + } + + init(locationModule: LocationModule?) { + self.locationModule = locationModule + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidLoad() { + super.viewDidLoad() + initializeMap() + self.view.addSubview(mapView) + } + + /// Initialize map with locations and optional reloading + private func initializeMap() { + guard let locationModule else { + return + } + + self.mapView.mapboxMap.onNext(event: .mapLoaded) { _ in + let locations = locationModule.allLocations + do { + try self.addGeoJSONSource(with: locations) + try self.addCircleLayer(sourceId: Constants.geoSourceId) + self.centerCamera(at: locations.last, zoomLevel: Constants.zoomLevel) + self.setupDynamicLocationUpdates() + } catch { + print("[MapboxMap] Error: \(error.localizedDescription)") + } + } + } + + /// Add GeoJSON source to the map + private func addGeoJSONSource(with locations: [CLLocationCoordinate2D]) throws { + var source = GeoJSONSource(id: Constants.geoSourceId) + source.data = .feature(Feature(geometry: .multiPoint(MultiPoint(locations)))) + try mapView.mapboxMap.addSource(source) + } + + /// Add circle layer to the map + private func addCircleLayer(sourceId: String) throws { + var circlesLayer = CircleLayer(id: Constants.circleLayerId, source: sourceId) + circlesLayer.circleColor = .constant(StyleColor(.red)) + circlesLayer.circleStrokeColor = .constant(StyleColor(.black)) + circlesLayer.circleStrokeWidth = .constant(2) + try mapView.mapboxMap.addLayer(circlesLayer) + } + + /// Center the map's camera + private func centerCamera(at location: CLLocationCoordinate2D?, zoomLevel: Double) { + guard let center = location else { + return + } + + mapView.mapboxMap.setCamera(to: CameraOptions(center: center, zoom: zoomLevel)) + } + + /// Set up dynamic updates for locations + private func setupDynamicLocationUpdates() { + guard let locationModule else { + return + } + + locationModule.onLocationsUpdated = { locations in + self.mapView.mapboxMap.updateGeoJSONSource( + withId: Constants.geoSourceId, + geoJSON: .feature(Feature(geometry: .lineString(LineString(locations)))) + ) + self.centerCamera(at: locations.last, zoomLevel: Constants.zoomLevel) + } + } +} diff --git a/StrokeCog/Map/StrokeCogMapView.swift b/StrokeCog/Map/StrokeCogMapView.swift new file mode 100644 index 0000000..44e6bad --- /dev/null +++ b/StrokeCog/Map/StrokeCogMapView.swift @@ -0,0 +1,31 @@ +// +// HomeView.swift +// StrokeCog +// +// Created by Vishnu Ravi on 4/2/24. +// Copyright © 2024 StrokeCog. All rights reserved. +// + +import MapboxMaps +import SwiftUI + +struct StrokeCogMapView: View { + @AppStorage(StorageKeys.trackingPreference) private var trackingOn = true + + @State private var showingSurveyAlert = false + @State private var alertMessage = "" + @State private var showingSurvey = false + @State private var optionsPanelOpen = true + + var body: some View { + ZStack { + MapManagerViewWrapper() + } + } +} + +struct StrokeCogMapView_Previews: PreviewProvider { + static var previews: some View { + StrokeCogMapView() + } +} diff --git a/StrokeCog/Onboarding/LocationPermissions.swift b/StrokeCog/Onboarding/LocationPermissions.swift new file mode 100644 index 0000000..4705acd --- /dev/null +++ b/StrokeCog/Onboarding/LocationPermissions.swift @@ -0,0 +1,84 @@ +// +// LocationPermissions.swift +// StrokeCog +// +// Created by Vishnu Ravi on 4/2/24. +// + +import OSLog +import SpeziOnboarding +import SpeziScheduler +import SwiftUI + + +struct LocationPermissions: View { + @Environment(OnboardingNavigationPath.self) private var onboardingNavigationPath + @Environment(LocationModule.self) private var locationModule + + @State private var locationProcessing = false + + private let logger = Logger(subsystem: "StrokeCog", category: "Onboarding") + + + var body: some View { + OnboardingView( + contentView: { + VStack { + OnboardingTitleView( + title: "LOCATION_PERMISSIONS_TITLE", + subtitle: "LOCATION_PERMISSIONS_SUBTITLE" + ) + Spacer() + Image(systemName: "mappin.and.ellipse") + .font(.system(size: 150)) + .foregroundColor(.accentColor) + .accessibilityHidden(true) + Text("LOCATION_PERMISSIONS_DESCRIPTION") + .multilineTextAlignment(.center) + .padding(.vertical, 16) + Spacer() + } + }, actionView: { + OnboardingActionsView( + "LOCATION_PERMISSIONS_BUTTON", + action: { + do { + locationProcessing = true + // Location authorization is not available in the preview simulator. + if ProcessInfo.processInfo.isPreviewSimulator { + try await _Concurrency.Task.sleep(for: .seconds(5)) + locationProcessing = false + } else { + locationModule.requestAuthorizationLocation() + } + } catch { + logger.debug("Could not request location permissions.") + } + } + ) + } + ) + .navigationBarBackButtonHidden(locationProcessing) + .navigationTitle(Text(verbatim: "")) + .onReceive(locationModule.$authorizationStatus) { status in + switch status { + case .authorizedWhenInUse: + locationModule.requestAuthorizationLocation() + case .authorizedAlways: + onboardingNavigationPath.nextStep() + locationProcessing = false + default: + break + } + } + } +} + + +#if DEBUG +#Preview { + OnboardingStack { + LocationPermissions() + } +} +#endif diff --git a/StrokeCog/Onboarding/OnboardingFlow.swift b/StrokeCog/Onboarding/OnboardingFlow.swift index 65586d3..ffbdc98 100644 --- a/StrokeCog/Onboarding/OnboardingFlow.swift +++ b/StrokeCog/Onboarding/OnboardingFlow.swift @@ -54,6 +54,8 @@ struct OnboardingFlow: View { if !localNotificationAuthorization { NotificationPermissions() } + + LocationPermissions() } .task { localNotificationAuthorization = await scheduler.localNotificationAuthorization diff --git a/StrokeCog/Onboarding/StudyIDView.swift b/StrokeCog/Onboarding/StudyIDView.swift index fbdf415..cd04d2f 100644 --- a/StrokeCog/Onboarding/StudyIDView.swift +++ b/StrokeCog/Onboarding/StudyIDView.swift @@ -21,8 +21,8 @@ struct StudyIDView: View { OnboardingView( titleView: { OnboardingTitleView( - title: "Study ID", - subtitle: "Welcome to the StrokeCog study. To get started, please enter your study ID." + title: "STUDYID_TITLE", + subtitle: "STUDYID_SUBTITLE" ) }, contentView: { @@ -50,7 +50,7 @@ struct StudyIDView: View { @ViewBuilder private var studyIDEntryView: some View { VerifiableTextField( - LocalizedStringResource("Tap here and enter your Study ID"), + LocalizedStringResource("STUDYID_TEXT_FIELD_LABEL"), text: $studyID ) .autocorrectionDisabled() @@ -62,8 +62,8 @@ struct StudyIDView: View { "Error", isPresented: $showInvalidIDAlert ) { - Text("You've entered an invalid study ID.") - Button("Try Again") { } + Text("INVALID_STUDYID_MESSAGE") + Button("RETRY_BUTTON_LABEL") { } } } @@ -72,7 +72,7 @@ struct StudyIDView: View { rule: { studyID in studyID.count >= 4 }, - message: "A study ID should be at least 4 characters long." + message: "STUDYID_VALIDATION_MESSAGE" ) } diff --git a/StrokeCog/Resources/Localizable.xcstrings b/StrokeCog/Resources/Localizable.xcstrings index 399ec22..241a422 100644 --- a/StrokeCog/Resources/Localizable.xcstrings +++ b/StrokeCog/Resources/Localizable.xcstrings @@ -1,9 +1,6 @@ { "sourceLanguage" : "en", "strings" : { - "A study ID should be at least 4 characters long." : { - - }, "ACCOUNT_NEXT" : { "localizations" : { "en" : { @@ -252,6 +249,16 @@ } } }, + "INVALID_STUDYID_MESSAGE" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You entered an invalid Study ID." + } + } + } + }, "LELAND_STANFORD_BIO" : { "localizations" : { "en" : { @@ -271,6 +278,49 @@ } } } + }, + "LOCATION_PERMISSIONS_BUTTON" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Allow Sharing Location" + } + } + } + }, + "LOCATION_PERMISSIONS_DESCRIPTION" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The app will track your location while you are in the study." + } + } + } + }, + "LOCATION_PERMISSIONS_SUBTITLE" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "StrokeCog Location" + } + } + } + }, + "LOCATION_PERMISSIONS_TITLE" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Location" + } + } + } + }, + "Map" : { + }, "MOCK_WEB_SERVICE_TAB_TITLE" : { "comment" : "MARK: - Mock Upload Data Storage Provider", @@ -347,6 +397,16 @@ } } }, + "RETRY_BUTTON_LABEL" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Try Again" + } + } + } + }, "SCHEDULE_LIST_TITLE" : { "localizations" : { "en" : { @@ -368,11 +428,45 @@ } } }, - "Study ID" : { - + "STUDYID_SUBTITLE" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Welcome to the StrokeCog study. Please enter your study ID below." + } + } + } }, - "Tap here and enter your Study ID" : { - + "STUDYID_TEXT_FIELD_LABEL" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tap here and enter your Study ID" + } + } + } + }, + "STUDYID_TITLE" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Study ID" + } + } + } + }, + "STUDYID_VALIDATION_MESSAGE" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "A study ID should be at least 4 characters long." + } + } + } }, "TASK_CONTEXT_ACTION_QUESTIONNAIRE" : { "localizations" : { @@ -423,12 +517,6 @@ } } } - }, - "Try Again" : { - - }, - "Welcome to the StrokeCog study. To get started, please enter your study ID." : { - }, "WELCOME_AREA1_DESCRIPTION" : { "localizations" : { @@ -520,9 +608,6 @@ } } } - }, - "You've entered an invalid study ID." : { - } }, "version" : "1.0" diff --git a/StrokeCog/SharedContext/Constants.swift b/StrokeCog/SharedContext/Constants.swift new file mode 100644 index 0000000..e444e66 --- /dev/null +++ b/StrokeCog/SharedContext/Constants.swift @@ -0,0 +1,37 @@ +// +// Constants.swift +// StrokeCog +// +// Created by Vishnu Ravi on 4/2/24. +// + +enum Constants { + static let prefConfirmedLogin = "PREF_CONFIRMED_LOGIN" + static let prefFirstRunWasMarked = "PREF_FIRST_RUN" + static let prefUserEmail = "PREF_USER_EMAIL" + static let prefStudyID = "PREF_STUDY_ID" + static let prefsNotificationsSchedule = "PREFS_NOTIFICATIONS_SCHEDULE" + static let prefTrackingStatus = "PREF_TRACKING_STATUS" + + static let prefCareKitCoreDataInitDate = "PREF_CORE_DATA_INIT_DATE" + static let prefHealthRecordsLastUploaded = "PREF_HEALTH_LAST_UPLOAD" + + static let notificationUserLogin = "NOTIFICATION_USER_LOGIN" + + static let dataBucketUserDetails = "userDetails" + static let dataBucketSurveys = "ls_surveys" + static let dataBucketHealthKit = "healthKit" + static let dataBucketStorage = "storage" + static let dataBucketMetrics = "metrics" + + static let onboardingDidComplete = "didCompleteOnboarding" + + static let JHFirstLocationRequest = "JHFirstLocationRequest" + + static let lastSurveyDate = "LAST_SURVEY_DATE" + static let hourToOpenSurvey = 19 // Hour to open survey daily in military time + + static let minDistanceBetweenPoints = 100.0 // minimum distance between location points to record + + static let privacyPolicyURL = "https://michelleodden.com/cardinal-lifespace-privacy-policy/" +} diff --git a/StrokeCog/SharedContext/StorageKeys.swift b/StrokeCog/SharedContext/StorageKeys.swift index 2cc5925..4cad30f 100644 --- a/StrokeCog/SharedContext/StorageKeys.swift +++ b/StrokeCog/SharedContext/StorageKeys.swift @@ -18,4 +18,6 @@ enum StorageKeys { // MARK: - Home /// The currently selected home tab. static let homeTabSelection = "home.tabselection" + + static let trackingPreference = "tracking.preference" } diff --git a/StrokeCog/StrokeCogDelegate.swift b/StrokeCog/StrokeCogDelegate.swift index 9568c30..3821b4a 100644 --- a/StrokeCog/StrokeCogDelegate.swift +++ b/StrokeCog/StrokeCogDelegate.swift @@ -50,6 +50,7 @@ class StrokeCogDelegate: SpeziAppDelegate { StrokeCogScheduler() OnboardingDataSource() + LocationModule() } } diff --git a/StrokeCog/StrokeCogStandard.swift b/StrokeCog/StrokeCogStandard.swift index 12f4e7c..b0c68b4 100644 --- a/StrokeCog/StrokeCogStandard.swift +++ b/StrokeCog/StrokeCogStandard.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import CoreLocation import FirebaseFirestore import FirebaseStorage import HealthKitOnFHIR @@ -115,6 +116,26 @@ actor StrokeCogStandard: Standard, EnvironmentAccessible, HealthKitConstraint, O } } + func add(location: CLLocationCoordinate2D) async throws { + guard let details = await account.details else { + throw StrokeCogStandardError.userNotAuthenticatedYet + } + + let dataPoint = LocationDataPoint( + currentDate: Date(), + time: Date().timeIntervalSince1970, + latitude: location.latitude, + longitude: location.longitude, + studyID: details.userId, + updatedBy: details.accountId + ) + + try await userDocumentReference + .collection("location_data") + .document(UUID().uuidString) + .setData(from: dataPoint) + } + private func healthKitDocument(id uuid: UUID) async throws -> DocumentReference { try await userDocumentReference diff --git a/StrokeCog/Supporting Files/Info.plist b/StrokeCog/Supporting Files/Info.plist index 5dc3321..93baf58 100644 --- a/StrokeCog/Supporting Files/Info.plist +++ b/StrokeCog/Supporting Files/Info.plist @@ -2,8 +2,14 @@ + CFBundleAllowMixedLocalizations + ITSAppUsesNonExemptEncryption + MBXAccessToken + TOKEN + MGLMapboxMetricsEnabledSettingShownInApp + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes @@ -11,7 +17,9 @@ UISceneConfigurations - CFBundleAllowMixedLocalizations - + UIBackgroundModes + + location +