From 32e2ee885f6ba0006a8846d45bb8059b0b4498dd Mon Sep 17 00:00:00 2001 From: Vishnu Ravi Date: Tue, 2 Apr 2024 04:22:12 -0400 Subject: [PATCH 01/15] Setup Mapbox library --- StrokeCog.xcodeproj/project.pbxproj | 37 +++++- .../xcshareddata/swiftpm/Package.resolved | 38 +++++- .../xcshareddata/xcschemes/StrokeCog.xcscheme | 4 +- .../CodableArray+RawRepresentable.swift | 28 ----- StrokeCog/Home.swift | 6 + StrokeCog/Map/MapView.swift | 119 ++++++++++++++++++ StrokeCog/Resources/Localizable.xcstrings | 18 +++ StrokeCog/SharedContext/Constants.swift | 38 ++++++ StrokeCog/SharedContext/StorageKeys.swift | 2 + 9 files changed, 255 insertions(+), 35 deletions(-) delete mode 100644 StrokeCog/Helper/CodableArray+RawRepresentable.swift create mode 100644 StrokeCog/Map/MapView.swift create mode 100644 StrokeCog/SharedContext/Constants.swift diff --git a/StrokeCog.xcodeproj/project.pbxproj b/StrokeCog.xcodeproj/project.pbxproj index 20c689d..8bf58a1 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,6 +66,9 @@ 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 /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6347EB632BBBA895008E0C4A /* MapView.swift */; }; + 6347EB6A2BBBF3AC008E0C4A /* MapboxMaps in Frameworks */ = {isa = PBXBuildFile; productRef = 6347EB692BBBF3AC008E0C4A /* MapboxMaps */; }; + 6347EB742BBBF442008E0C4A /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6347EB732BBBF442008E0C4A /* Constants.swift */; }; 63BBF8162BB8993B006890CE /* StudyIDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63BBF8152BB8993B006890CE /* StudyIDView.swift */; }; 63BBF8192BB89CF7006890CE /* studyIDs.csv in Resources */ = {isa = PBXBuildFile; fileRef = 63BBF8182BB89CF7006890CE /* studyIDs.csv */; }; 653A2551283387FE005D4D48 /* StrokeCog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A2550283387FE005D4D48 /* StrokeCog.swift */; }; @@ -127,7 +129,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,6 +143,8 @@ 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 /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = ""; }; + 6347EB732BBBF442008E0C4A /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.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 = ""; }; 653A254D283387FE005D4D48 /* StrokeCog.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StrokeCog.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -166,6 +169,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 */, @@ -282,6 +286,7 @@ children = ( 2FE5DC3E29EDD7ED004B9AB4 /* FeatureFlags.swift */, 2FE5DC3F29EDD7EE004B9AB4 /* StorageKeys.swift */, + 6347EB732BBBF442008E0C4A /* Constants.swift */, ); path = SharedContext; sourceTree = ""; @@ -291,7 +296,6 @@ children = ( 2FE5DC4229EDD7F2004B9AB4 /* Binding+Negate.swift */, 2FE5DC4329EDD7F2004B9AB4 /* Bundle+Image.swift */, - 2FE5DC4429EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift */, ); path = Helper; sourceTree = ""; @@ -307,6 +311,14 @@ path = Contributions; sourceTree = ""; }; + 6347EB622BBBA874008E0C4A /* Map */ = { + isa = PBXGroup; + children = ( + 6347EB632BBBA895008E0C4A /* MapView.swift */, + ); + path = Map; + sourceTree = ""; + }; 653A2544283387FE005D4D48 = { isa = PBXGroup; children = ( @@ -342,6 +354,7 @@ 2FE5DC3B29EDD7D0004B9AB4 /* Schedule */, 2FE5DC2729EDD38D004B9AB4 /* Contacts */, 56F6F29E2AB441640022FE5A /* Contributions */, + 6347EB622BBBA874008E0C4A /* Map */, 2F4FC8D529EE69BE00BFFE26 /* MockUpload */, 2FE5DC3C29EDD7DA004B9AB4 /* SharedContext */, 2FE5DC3D29EDD7E4004B9AB4 /* Helper */, @@ -431,6 +444,7 @@ 97D73D692AD860AD00B47FA0 /* SpeziFirebaseStorage */, A9D83F952B083794000D0C78 /* SpeziFirebaseAccountStorage */, A92E4DEF2BAA001100AC8DE8 /* OrderedCollections */, + 6347EB692BBBF3AC008E0C4A /* MapboxMaps */, ); productName = StrokeCog; productReference = 653A254D283387FE005D4D48 /* StrokeCog.app */; @@ -526,6 +540,7 @@ 2FB099B42A875E2B00B20952 /* XCRemoteSwiftPackageReference "HealthKitOnFHIR" */, 5661551B2AB8384200209B80 /* XCRemoteSwiftPackageReference "swift-package-list" */, A92E4DEE2BAA001100AC8DE8 /* XCRemoteSwiftPackageReference "swift-collections" */, + 6347EB662BBBF34A008E0C4A /* XCRemoteSwiftPackageReference "mapbox-maps-ios" */, ); productRefGroup = 653A254E283387FE005D4D48 /* Products */; projectDirPath = ""; @@ -608,13 +623,14 @@ 2FE5DC3729EDD7CA004B9AB4 /* OnboardingFlow.swift in Sources */, 2F1AC9DF2B4E840E00C24973 /* StrokeCog.docc in Sources */, 2FF53D8D2A8729D600042B76 /* StrokeCogStandard.swift in Sources */, - 2FE5DC4729EDD7F2004B9AB4 /* CodableArray+RawRepresentable.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 /* MapView.swift in Sources */, 2FE5DC5029EDD7FA004B9AB4 /* EventContextView.swift in Sources */, 2F4E23832989D51F0013F3D9 /* StrokeCogTestingSetup.swift in Sources */, + 6347EB742BBBF442008E0C4A /* Constants.swift in Sources */, 2FE5DC5329EDD7FA004B9AB4 /* Bundle+Questionnaire.swift in Sources */, 2FE5DC5129EDD7FA004B9AB4 /* StrokeCogTaskContext.swift in Sources */, 56F6F2A02AB441930022FE5A /* ContributionsList.swift in Sources */, @@ -1271,6 +1287,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 +1428,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..94508df 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" : "43e89b81bc5041bc458ff8ef663965df68e755e6e2fdcb3a11c344d1cc5c0249", "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.xcodeproj/xcshareddata/xcschemes/StrokeCog.xcscheme b/StrokeCog.xcodeproj/xcshareddata/xcschemes/StrokeCog.xcscheme index fcbb02d..4b87a49 100644 --- a/StrokeCog.xcodeproj/xcshareddata/xcschemes/StrokeCog.xcscheme +++ b/StrokeCog.xcodeproj/xcshareddata/xcschemes/StrokeCog.xcscheme @@ -79,7 +79,7 @@ + isEnabled = "YES"> + isEnabled = "NO"> 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/Home.swift b/StrokeCog/Home.swift index 1058049..b02fe84 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) { + MapView() + .tag(Tabs.map) + .tabItem { + Label("Map", systemImage: "map.circle") + } ScheduleView(presentingAccount: $presentingAccount) .tag(Tabs.schedule) .tabItem { diff --git a/StrokeCog/Map/MapView.swift b/StrokeCog/Map/MapView.swift new file mode 100644 index 0000000..11bbbd6 --- /dev/null +++ b/StrokeCog/Map/MapView.swift @@ -0,0 +1,119 @@ +// +// HomeView.swift +// LifeSpace +// +// Created by Vishnu Ravi on 5/18/22. +// Copyright © 2022 LifeSpace. All rights reserved. +// + +import SwiftUI +@_spi(Experimental) import MapboxMaps + +// swiftlint:disable closure_body_length file_types_order +struct MapView: View { + @State private var showingSurveyAlert = false + @State private var alertMessage = "" + @State private var showingSurvey = false + @AppStorage(StorageKeys.trackingPreference) private var trackingOn = true + @State private var optionsPanelOpen = true + + init() { + MapboxOptions.accessToken = "" + } + + var body: some View { + ZStack { + Map() + .ignoresSafeArea() + + // overlay buttons on map + VStack { + Spacer() + + GroupBox { + + Button { + withAnimation { + self.optionsPanelOpen.toggle() + } + } label: { + HStack { + Text("Options") + Spacer() + Image(systemName: self.optionsPanelOpen ? "chevron.down" : "chevron.up") + } + } + + if self.optionsPanelOpen { + GroupBox { + Button { + // First check if it's too early to take the survey, + // and if not, then check to make sure it hasn't been + // taken already today. +// if !SurveyRules.isAfterStartHour() { +// self.alertMessage = SurveyRules.tooEarlyMessage +// self.showingSurveyAlert.toggle() +// } else if !SurveyRules.wasNotTakenToday() { +// self.alertMessage = SurveyRules.alreadyTookSurveyMessage +// self.showingSurveyAlert.toggle() +// } else { +// self.showingSurvey.toggle() +// } + } label: { + Text("Take Daily Survey") + .foregroundColor(.white) + .frame(maxWidth: .infinity) + } + .alert(isPresented: $showingSurveyAlert) { + Alert(title: Text("Survey Not Available"), + message: Text(self.alertMessage), + dismissButton: .default(Text("OK"))) + } + .sheet(isPresented: $showingSurvey) { +// CKTaskViewController(tasks: DailySurveyTask(showInstructions: false)) +// .ignoresSafeArea(.container, edges: .bottom) +// .ignoresSafeArea(.keyboard, edges: .bottom) + } + }.groupBoxStyle(ButtonGroupBoxStyle()) + + GroupBox { + Toggle("Track My Location", isOn: $trackingOn) + .onChange(of: trackingOn) { _ in + if trackingOn { + // LocationService.shared.startTracking() + } else { + // LocationService.shared.stopTracking() + } + } + } + } + } + }.onAppear { + // Make sure last survey date is updated + async { + do { + // try await CKStudyUser.shared.getLastSurveyDate() + } catch { + print("Error updating last survey date.") + } + } + } + } + } +} + +struct ButtonGroupBoxStyle: GroupBoxStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.content + .frame(maxWidth: .infinity) + .padding() + .background(RoundedRectangle(cornerRadius: 8).fill(Color("primaryRed"))) + } +} + +struct HomeView_Previews: PreviewProvider { + static var previews: some View { + HomeView() + } +} + diff --git a/StrokeCog/Resources/Localizable.xcstrings b/StrokeCog/Resources/Localizable.xcstrings index 399ec22..649fd42 100644 --- a/StrokeCog/Resources/Localizable.xcstrings +++ b/StrokeCog/Resources/Localizable.xcstrings @@ -271,6 +271,9 @@ } } } + }, + "Map" : { + }, "MOCK_WEB_SERVICE_TAB_TITLE" : { "comment" : "MARK: - Mock Upload Data Storage Provider", @@ -326,6 +329,12 @@ } } } + }, + "OK" : { + + }, + "Options" : { + }, "PROJECT_LICENSE_DESCRIPTION" : { "localizations" : { @@ -370,6 +379,12 @@ }, "Study ID" : { + }, + "Survey Not Available" : { + + }, + "Take Daily Survey" : { + }, "Tap here and enter your Study ID" : { @@ -423,6 +438,9 @@ } } } + }, + "Track My Location" : { + }, "Try Again" : { diff --git a/StrokeCog/SharedContext/Constants.swift b/StrokeCog/SharedContext/Constants.swift new file mode 100644 index 0000000..f194522 --- /dev/null +++ b/StrokeCog/SharedContext/Constants.swift @@ -0,0 +1,38 @@ +// +// Constants.swift +// StrokeCog +// +// Created by Vishnu Ravi on 4/2/24. +// + +class 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" } From 34a5e0d411421b00c050405b6f659b0b0b6b49bc Mon Sep 17 00:00:00 2001 From: Vishnu Ravi Date: Tue, 2 Apr 2024 19:46:59 -0400 Subject: [PATCH 02/15] Add files from LifeSpace project --- StrokeCog.xcodeproj/project.pbxproj | 43 +++- StrokeCog/Helper/Date+Helpers.swift | 251 ++++++++++++++++++++++ StrokeCog/Home.swift | 2 +- StrokeCog/Map/LocationService.swift | 180 ++++++++++++++++ StrokeCog/Map/LocationUtils.swift | 33 +++ StrokeCog/Map/MapView.swift | 119 ---------- StrokeCog/Map/MapboxMap.swift | 61 ++++++ StrokeCog/Map/MapboxView.swift | 33 +++ StrokeCog/Map/StrokeCogMapView.swift | 45 ++++ StrokeCog/Resources/Localizable.xcstrings | 15 -- StrokeCog/Supporting Files/Info.plist | 8 +- 11 files changed, 643 insertions(+), 147 deletions(-) create mode 100644 StrokeCog/Helper/Date+Helpers.swift create mode 100644 StrokeCog/Map/LocationService.swift create mode 100644 StrokeCog/Map/LocationUtils.swift delete mode 100644 StrokeCog/Map/MapView.swift create mode 100644 StrokeCog/Map/MapboxMap.swift create mode 100644 StrokeCog/Map/MapboxView.swift create mode 100644 StrokeCog/Map/StrokeCogMapView.swift diff --git a/StrokeCog.xcodeproj/project.pbxproj b/StrokeCog.xcodeproj/project.pbxproj index 8bf58a1..af230df 100644 --- a/StrokeCog.xcodeproj/project.pbxproj +++ b/StrokeCog.xcodeproj/project.pbxproj @@ -66,11 +66,16 @@ 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 /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6347EB632BBBA895008E0C4A /* MapView.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 */; }; 63BBF8162BB8993B006890CE /* StudyIDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63BBF8152BB8993B006890CE /* StudyIDView.swift */; }; 63BBF8192BB89CF7006890CE /* studyIDs.csv in Resources */ = {isa = PBXBuildFile; fileRef = 63BBF8182BB89CF7006890CE /* studyIDs.csv */; }; + 63F4C3972BBCCC070033D985 /* MapboxMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F4C3962BBCCC070033D985 /* MapboxMap.swift */; }; + 63F4C3992BBCCC300033D985 /* MapboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F4C3982BBCCC300033D985 /* MapboxView.swift */; }; + 63F4C39B2BBCCCF80033D985 /* LocationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F4C39A2BBCCCF80033D985 /* LocationService.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 */; }; 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 */; }; @@ -143,10 +148,15 @@ 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 /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.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 = ""; }; 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 = ""; }; + 63F4C3962BBCCC070033D985 /* MapboxMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapboxMap.swift; sourceTree = ""; }; + 63F4C3982BBCCC300033D985 /* MapboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapboxView.swift; sourceTree = ""; }; + 63F4C39A2BBCCCF80033D985 /* LocationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationService.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 = ""; }; 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 = ""; }; @@ -296,6 +306,7 @@ children = ( 2FE5DC4229EDD7F2004B9AB4 /* Binding+Negate.swift */, 2FE5DC4329EDD7F2004B9AB4 /* Bundle+Image.swift */, + 63F4C39E2BBCCDB70033D985 /* Date+Helpers.swift */, ); path = Helper; sourceTree = ""; @@ -314,7 +325,11 @@ 6347EB622BBBA874008E0C4A /* Map */ = { isa = PBXGroup; children = ( - 6347EB632BBBA895008E0C4A /* MapView.swift */, + 6347EB632BBBA895008E0C4A /* StrokeCogMapView.swift */, + 63F4C3962BBCCC070033D985 /* MapboxMap.swift */, + 63F4C3982BBCCC300033D985 /* MapboxView.swift */, + 63F4C39A2BBCCCF80033D985 /* LocationService.swift */, + 63F4C39C2BBCCD200033D985 /* LocationUtils.swift */, ); path = Map; sourceTree = ""; @@ -627,10 +642,15 @@ 2FE5DC4029EDD7EE004B9AB4 /* FeatureFlags.swift in Sources */, 2FE5DC4629EDD7F2004B9AB4 /* Bundle+Image.swift in Sources */, 2FE5DC4F29EDD7FA004B9AB4 /* EventContext.swift in Sources */, - 6347EB642BBBA895008E0C4A /* MapView.swift in Sources */, + 6347EB642BBBA895008E0C4A /* StrokeCogMapView.swift in Sources */, + 63F4C3972BBCCC070033D985 /* MapboxMap.swift in Sources */, + 63F4C3992BBCCC300033D985 /* MapboxView.swift in Sources */, 2FE5DC5029EDD7FA004B9AB4 /* EventContextView.swift in Sources */, + 63F4C39F2BBCCDB70033D985 /* Date+Helpers.swift in Sources */, 2F4E23832989D51F0013F3D9 /* StrokeCogTestingSetup.swift in Sources */, + 63F4C39B2BBCCCF80033D985 /* LocationService.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 */, @@ -769,8 +789,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."; @@ -971,8 +992,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."; @@ -1018,8 +1040,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."; diff --git a/StrokeCog/Helper/Date+Helpers.swift b/StrokeCog/Helper/Date+Helpers.swift new file mode 100644 index 0000000..ffa336d --- /dev/null +++ b/StrokeCog/Helper/Date+Helpers.swift @@ -0,0 +1,251 @@ +// +// Date+Helpers.swift +// CS342Support +// +// Created by Santiago Gutierrez on 9/22/19. +// Copyright © 2019 Stanford University. All rights reserved. +// + +import Foundation + +extension Date { + + public func fullFormattedString() -> String { + return DateFormatter.localizedString(from: self, dateStyle: .full, timeStyle: .none) + } + + public func shortFormattedString() -> String { + return DateFormatter.localizedString(from: self, dateStyle: .short, timeStyle: .none) + } + + public var yesterday: Date { + return dayByAdding(-1) ?? Date().addingTimeInterval(-86400) + } + + public var tomorrow: Date { + return dayByAdding(1) ?? Date().addingTimeInterval(86400) + } + + public var startOfDay: Date { + let cal = Calendar(identifier: Calendar.Identifier.gregorian) + return cal.startOfDay(for: self) + } + + public var endOfDay: Date? { + let calendar = Calendar.current + let components = (calendar as NSCalendar).components([.year, .month, .day], from: self) + + guard let startOfDay = calendar.date(from: components) else { + fatalError("*** Unable to create the start date ***") + } + return (calendar as NSCalendar).date(byAdding: .day, value: 1, to: startOfDay, options: []) + } + + public func ISOStringFromDate() -> String { + let dateFormatter = DateFormatter() + let timeZone = TimeZone(secondsFromGMT: 0) + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" + dateFormatter.timeZone = timeZone + + return dateFormatter.string(from: self) + "Z" //this is in UTC, 0 seconds from GMT. + } + + public func shortStringFromDate() -> String { + let dateFormatter = DateFormatter() + let timeZone = TimeZone(secondsFromGMT: 0) + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = "MM-dd-yyyy" + dateFormatter.timeZone = timeZone + + return dateFormatter.string(from: self) + } + + public func dayByAdding(_ daysToAdd: Int) -> Date? { + let calendar = Calendar.current + let components = (calendar as NSCalendar).components([.year, .month, .day], from: self) + + guard let startOfDay = calendar.date(from: components) else { + fatalError("*** Unable to create the start date ***") + } + return (calendar as NSCalendar).date(byAdding: .day, value: daysToAdd, to: startOfDay, options: []) + } + +} + +extension Date { + fileprivate static func componentFlags() -> NSCalendar.Unit { return [.year, .month, .day, .weekday] } + + static func getTodayHour() -> String { + let calendar = Calendar(identifier: Calendar.Identifier.gregorian) + let components = (calendar as NSCalendar).components([.hour, .minute], from: Date()) + let hour = components.hour ?? 0 + let minutes = components.minute ?? 0 + return "Today, \(hour):\(minutes)" + } + + func getDay() -> Int { + let formatter = DateFormatter() + formatter.dateFormat = "dd-MM-yyyy" + let calendar = Calendar(identifier: Calendar.Identifier.gregorian) + let components = (calendar as NSCalendar).components(Date.componentFlags(), from: self) + let day = components.day + return day! + } + + func getMonth() -> String { + let formatter = DateFormatter() + formatter.dateFormat = "dd-MM-yyyy" + let calendar = Calendar(identifier: Calendar.Identifier.gregorian) + let components = (calendar as NSCalendar).components(Date.componentFlags(), from: self) + let month = formatter.shortMonthSymbols[components.month!-1] + return month + } + + func timeToString() -> String { + return DateFormatter.localizedString(from: self, dateStyle: .none, timeStyle: .short) + } + + func longFormattedString(includeTime: Bool = false) -> String { + return DateFormatter.localizedString(from: self, dateStyle: .long, timeStyle: includeTime ? .long : .none) + } + + static func dateFromComponents(_ components: DateComponents) -> Date? { + let calendar = Calendar(identifier: Calendar.Identifier.gregorian) + return calendar.date(from: components) + } + + static func dateFromString(_ string: String) -> Date? { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss-SSS" + + if let stringDate = dateFormatter.date(from: string) { + return stringDate + } else { + return nil + } + } + + //returns a string with a specific format, conforming to UTC + func stringWithFormat(_ format: String = "yyyy-MM-dd'T'HH:mm:ss.SSS") -> String { + let dateFormatter = DateFormatter() + let timeZone = TimeZone(secondsFromGMT: 0) + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = format + dateFormatter.timeZone = timeZone + + return dateFormatter.string(from: self) + } + + //returns a string with ISO format conforming to local timezone. + func localStringFromDate() -> String { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" + dateFormatter.timeZone = TimeZone.current + + return dateFormatter.string(from: self) + } + + static func dateFromISOString(_ string: String) -> Date? { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" + //dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" + + + if let stringDate = dateFormatter.date(from: string) { + return stringDate + } else { + return nil + } + } + + public func isLessThanOneMinute(_ dateToCompare: Date) -> Bool { + if abs(self.minutesFrom(dateToCompare)) < 1 { + return true + } + return false + } + + func minutesFrom(_ dateToCompare: Date) -> Int { + return (Calendar.current as NSCalendar).components([.minute], from: dateToCompare, to: self, options: []).minute! + } + + func daysTo(_ dateToCompare: Date) -> Int { + return (Calendar.current as NSCalendar).components([.day], from: self, to: dateToCompare, options: []).day! + } + + func monthByAdding(_ monthsToAdd: Int) -> Date? { + let calendar = Calendar.current + return (calendar as NSCalendar).date(byAdding: .month, value: monthsToAdd, to: self, options: []) + } + + func startOfMonth() -> Date { + let calendar = Calendar.current; + let components = (calendar as NSCalendar).components([.year, .month], from: self); + + guard let startOfMonth = calendar.date(from: components) else { + fatalError("*** Unable to create the start date ***") + } + + return startOfMonth; + } + + func endOfMonth() -> Date? { + let calendar = Calendar.current + let components = (calendar as NSCalendar).components([.year, .month], from: self) + + guard let startOfMonth = calendar.date(from: components) else { + fatalError("*** Unable to create the start date ***") + } + return (calendar as NSCalendar).date(byAdding: .month, value: 1, to: startOfMonth, options: []) + } +} + +extension Date { + /// Returns the amount of years from another date + func years(from date: Date) -> Int { + return Calendar.current.dateComponents([.year], from: date, to: self).year ?? 0 + } + /// Returns the amount of months from another date + func months(from date: Date) -> Int { + return Calendar.current.dateComponents([.month], from: date, to: self).month ?? 0 + } + /// Returns the amount of weeks from another date + func weeks(from date: Date) -> Int { + return Calendar.current.dateComponents([.weekOfMonth], from: date, to: self).weekOfMonth ?? 0 + } + /// Returns the amount of days from another date + func days(from date: Date) -> Int { + return Calendar.current.dateComponents([.day], from: date, to: self).day ?? 0 + } + /// Returns the amount of hours from another date + func hours(from date: Date) -> Int { + return Calendar.current.dateComponents([.hour], from: date, to: self).hour ?? 0 + } + /// Returns the amount of minutes from another date + func minutes(from date: Date) -> Int { + return Calendar.current.dateComponents([.minute], from: date, to: self).minute ?? 0 + } + /// Returns the amount of seconds from another date + func seconds(from date: Date) -> Int { + return Calendar.current.dateComponents([.second], from: date, to: self).second ?? 0 + } + /// Returns the amount of nanoseconds from another date + func nanoseconds(from date: Date) -> Int { + return Calendar.current.dateComponents([.nanosecond], from: date, to: self).nanosecond ?? 0 + } + /// Returns the a custom time interval description from another date + func offset(from date: Date) -> String { + if years(from: date) > 0 { return "\(years(from: date))y" } + if months(from: date) > 0 { return "\(months(from: date))M" } + if weeks(from: date) > 0 { return "\(weeks(from: date))w" } + if days(from: date) > 0 { return "\(days(from: date))d" } + if hours(from: date) > 0 { return "\(hours(from: date))h" } + if minutes(from: date) > 0 { return "\(minutes(from: date))m" } + if seconds(from: date) > 0 { return "\(seconds(from: date))s" } + if nanoseconds(from: date) > 0 { return "\(nanoseconds(from: date))ns" } + return "" + } +} diff --git a/StrokeCog/Home.swift b/StrokeCog/Home.swift index b02fe84..5b67296 100644 --- a/StrokeCog/Home.swift +++ b/StrokeCog/Home.swift @@ -30,7 +30,7 @@ struct HomeView: View { var body: some View { TabView(selection: $selectedTab) { - MapView() + StrokeCogMapView() .tag(Tabs.map) .tabItem { Label("Map", systemImage: "map.circle") diff --git a/StrokeCog/Map/LocationService.swift b/StrokeCog/Map/LocationService.swift new file mode 100644 index 0000000..d6139fd --- /dev/null +++ b/StrokeCog/Map/LocationService.swift @@ -0,0 +1,180 @@ +// +// LocationService.swift +// LifeSpace +// +// Created by Vishnu Ravi on 4/2/24. + +import CoreLocation +import Firebase +import Foundation + +class LocationService: NSObject, CLLocationManagerDelegate, ObservableObject { + static let shared = LocationService() + + private let 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 init() { + super.init() + manager.delegate = self + + calculateIfCanShowRequestMessage() + + // 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() + } + } + + 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.") + } + } + + func stopTracking() { + self.manager.stopUpdatingLocation() + self.manager.stopMonitoringSignificantLocationChanges() + print("[LIFESPACE] Stopping tracking...") + } + + func calculateIfCanShowRequestMessage() { + let previousState = authorizationStatus + authorizationStatus = self.manager.authorizationStatus + + let first = UserDefaults.standard.bool(forKey: Constants.JHFirstLocationRequest) + + if authorizationStatus == .authorizedWhenInUse && !first && previousState != authorizationStatus { + UserDefaults.standard.set(true, forKey: Constants.JHFirstLocationRequest) + } else { + UserDefaults.standard.set(false, forKey: Constants.JHFirstLocationRequest) + } + + if authorizationStatus == .authorizedAlways { + // LaunchModel.sharedinstance.showPermissionView = false + } + + if self.manager.authorizationStatus == .notDetermined || + (self.manager.authorizationStatus == .authorizedWhenInUse && UserDefaults.standard.bool(forKey: Constants.JHFirstLocationRequest)) { + canShowRequestMessage = true + } else { + canShowRequestMessage = false + } + } + + 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()) { +// JHMapDataManager.shared.getAllMapPoints(date: date, onCompletion: {(results) in +// if let results = results as? [CLLocationCoordinate2D] { +// self.allLocations = results +// self.onLocationsUpdated?(self.allLocations) +// } +// }) +// } + + /// 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() + +// // write this location to the database +// if let mapPointsCollection = CKStudyUser.shared.mapPointsCollection, +// let user = CKStudyUser.shared.currentUser, +// let studyID = CKStudyUser.shared.studyID { +// let db = Firestore.firestore() +// db.collection(mapPointsCollection) +// .document(UUID().uuidString) +// .setData([ +// "currentdate": NSDate(), +// "time": NSDate().timeIntervalSince1970, +// "latitude": point.latitude, +// "longitude": point.longitude, +// "studyID": studyID, +// "UpdatedBy": user.uid +// ]) { err in +// if let err = err { +// print("[LIFESPACE] Error writing location to database: \(err)") +// } +// } +// } else { +// print("[LIFESPACE] Unable to save point due to missing metadata.") +// } + } + } + + func userAuthorizeAlways() -> Bool { + return self.manager.authorizationStatus == .authorizedAlways + } + + 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 + } + + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + self.calculateIfCanShowRequestMessage() + } +} diff --git a/StrokeCog/Map/LocationUtils.swift b/StrokeCog/Map/LocationUtils.swift new file mode 100644 index 0000000..8fef0b0 --- /dev/null +++ b/StrokeCog/Map/LocationUtils.swift @@ -0,0 +1,33 @@ +// +// LocationUtils.swift +// LifeSpace +// +// Created by Vishnu Ravi on 7/12/22. +// Copyright © 2022 CocoaPods. All rights reserved. +// + +import CoreLocation +import Foundation + +class 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 + public 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/MapView.swift b/StrokeCog/Map/MapView.swift deleted file mode 100644 index 11bbbd6..0000000 --- a/StrokeCog/Map/MapView.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// HomeView.swift -// LifeSpace -// -// Created by Vishnu Ravi on 5/18/22. -// Copyright © 2022 LifeSpace. All rights reserved. -// - -import SwiftUI -@_spi(Experimental) import MapboxMaps - -// swiftlint:disable closure_body_length file_types_order -struct MapView: View { - @State private var showingSurveyAlert = false - @State private var alertMessage = "" - @State private var showingSurvey = false - @AppStorage(StorageKeys.trackingPreference) private var trackingOn = true - @State private var optionsPanelOpen = true - - init() { - MapboxOptions.accessToken = "" - } - - var body: some View { - ZStack { - Map() - .ignoresSafeArea() - - // overlay buttons on map - VStack { - Spacer() - - GroupBox { - - Button { - withAnimation { - self.optionsPanelOpen.toggle() - } - } label: { - HStack { - Text("Options") - Spacer() - Image(systemName: self.optionsPanelOpen ? "chevron.down" : "chevron.up") - } - } - - if self.optionsPanelOpen { - GroupBox { - Button { - // First check if it's too early to take the survey, - // and if not, then check to make sure it hasn't been - // taken already today. -// if !SurveyRules.isAfterStartHour() { -// self.alertMessage = SurveyRules.tooEarlyMessage -// self.showingSurveyAlert.toggle() -// } else if !SurveyRules.wasNotTakenToday() { -// self.alertMessage = SurveyRules.alreadyTookSurveyMessage -// self.showingSurveyAlert.toggle() -// } else { -// self.showingSurvey.toggle() -// } - } label: { - Text("Take Daily Survey") - .foregroundColor(.white) - .frame(maxWidth: .infinity) - } - .alert(isPresented: $showingSurveyAlert) { - Alert(title: Text("Survey Not Available"), - message: Text(self.alertMessage), - dismissButton: .default(Text("OK"))) - } - .sheet(isPresented: $showingSurvey) { -// CKTaskViewController(tasks: DailySurveyTask(showInstructions: false)) -// .ignoresSafeArea(.container, edges: .bottom) -// .ignoresSafeArea(.keyboard, edges: .bottom) - } - }.groupBoxStyle(ButtonGroupBoxStyle()) - - GroupBox { - Toggle("Track My Location", isOn: $trackingOn) - .onChange(of: trackingOn) { _ in - if trackingOn { - // LocationService.shared.startTracking() - } else { - // LocationService.shared.stopTracking() - } - } - } - } - } - }.onAppear { - // Make sure last survey date is updated - async { - do { - // try await CKStudyUser.shared.getLastSurveyDate() - } catch { - print("Error updating last survey date.") - } - } - } - } - } -} - -struct ButtonGroupBoxStyle: GroupBoxStyle { - func makeBody(configuration: Configuration) -> some View { - configuration.content - .frame(maxWidth: .infinity) - .padding() - .background(RoundedRectangle(cornerRadius: 8).fill(Color("primaryRed"))) - } -} - -struct HomeView_Previews: PreviewProvider { - static var previews: some View { - HomeView() - } -} - diff --git a/StrokeCog/Map/MapboxMap.swift b/StrokeCog/Map/MapboxMap.swift new file mode 100644 index 0000000..6095874 --- /dev/null +++ b/StrokeCog/Map/MapboxMap.swift @@ -0,0 +1,61 @@ +// +// MapboxMap.swift +// StrokeCog +// +// Created by Vishnu Ravi on 4/2/24. +// + +import Foundation +import MapboxMaps + +// swiftlint:disable closure_body_length +class MapboxMap { + public static func initializeMap (mapView: MapView, reload: Bool) { + mapView.mapboxMap.onNext(event: .mapLoaded) { _ in + var locationsPoints = [CLLocationCoordinate2D]() + locationsPoints = LocationService.shared.allLocations + do { + var source = GeoJSONSource(id: "GEOSOURCE") + source.data = .feature(Feature(geometry: .multiPoint(MultiPoint(locationsPoints)))) + try mapView.mapboxMap.style.addSource(source) + + var circlesLayer = CircleLayer(id: "CIRCLELAYER", source: "GEOSOURCE") + circlesLayer.circleColor = .constant(StyleColor(.red)) + circlesLayer.circleStrokeColor = .constant(StyleColor(.black)) + circlesLayer.circleStrokeWidth = .constant(2) + try mapView.mapboxMap.style.addLayer(circlesLayer, layerPosition: .above("country-label")) + + mapView.mapboxMap.setCamera( + to: CameraOptions( + center: LocationService.shared.allLocations.last, + zoom: 14.0 + ) + ) + if reload { + LocationService.shared.onLocationsUpdated = { locations in + do { + try mapView.mapboxMap.style.updateGeoJSONSource( + withId: "GEOSOURCE", + geoJSON: .feature( + Feature( + geometry: .lineString(LineString(locations)) + ) + ) + ) + mapView.mapboxMap.setCamera( + to: CameraOptions( + center: LocationService.shared.allLocations.last, + zoom: 14.0 + ) + ) + } catch let error as NSError { + print("[LIFESPACE] Error updating map: \(error.localizedDescription)") + } + } + } + } catch let error as NSError { + print("[LIFESPACE] Error adding source or layer: \(error.localizedDescription)") + } + } + } +} diff --git a/StrokeCog/Map/MapboxView.swift b/StrokeCog/Map/MapboxView.swift new file mode 100644 index 0000000..530cabe --- /dev/null +++ b/StrokeCog/Map/MapboxView.swift @@ -0,0 +1,33 @@ +// +// MapboxView.swift +// StrokeCog +// +// Created by Vishnu Ravi on 4/2/24. +// + +import SwiftUI +import MapboxMaps + +struct MapManagerViewWrapper: UIViewControllerRepresentable { + typealias UIViewControllerType = MapManagerView + + func makeUIViewController(context: Context) -> MapManagerView { + return MapManagerView() + } + + func updateUIViewController(_ uiViewController: MapManagerView, context: Context) {} +} + +class MapManagerView: UIViewController { + internal var mapView: MapView! + + override public func viewDidLoad() { + super.viewDidLoad() + + mapView = MapView(frame: view.bounds) + mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + self.view.addSubview(mapView) + MapboxMap.initializeMap(mapView: mapView, reload: true) + mapView.location.options.puckType = .puck2D() + } +} diff --git a/StrokeCog/Map/StrokeCogMapView.swift b/StrokeCog/Map/StrokeCogMapView.swift new file mode 100644 index 0000000..94d7383 --- /dev/null +++ b/StrokeCog/Map/StrokeCogMapView.swift @@ -0,0 +1,45 @@ +// +// HomeView.swift +// LifeSpace +// +// Created by Vishnu Ravi on 5/18/22. +// Copyright © 2022 LifeSpace. All rights reserved. +// + +import SwiftUI +@_spi(Experimental) import MapboxMaps + +// swiftlint:disable closure_body_length file_types_order +struct StrokeCogMapView: View { + @State private var showingSurveyAlert = false + @State private var alertMessage = "" + @State private var showingSurvey = false + @AppStorage(StorageKeys.trackingPreference) private var trackingOn = true + @State private var optionsPanelOpen = true + + init() { + MapboxOptions.accessToken = "" + } + + var body: some View { + ZStack { + MapManagerViewWrapper() + } + } +} + +struct ButtonGroupBoxStyle: GroupBoxStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.content + .frame(maxWidth: .infinity) + .padding() + .background(RoundedRectangle(cornerRadius: 8).fill(Color("primaryRed"))) + } +} + +struct HomeView_Previews: PreviewProvider { + static var previews: some View { + HomeView() + } +} + diff --git a/StrokeCog/Resources/Localizable.xcstrings b/StrokeCog/Resources/Localizable.xcstrings index 649fd42..8244f32 100644 --- a/StrokeCog/Resources/Localizable.xcstrings +++ b/StrokeCog/Resources/Localizable.xcstrings @@ -329,12 +329,6 @@ } } } - }, - "OK" : { - - }, - "Options" : { - }, "PROJECT_LICENSE_DESCRIPTION" : { "localizations" : { @@ -379,12 +373,6 @@ }, "Study ID" : { - }, - "Survey Not Available" : { - - }, - "Take Daily Survey" : { - }, "Tap here and enter your Study ID" : { @@ -438,9 +426,6 @@ } } } - }, - "Track My Location" : { - }, "Try Again" : { diff --git a/StrokeCog/Supporting Files/Info.plist b/StrokeCog/Supporting Files/Info.plist index 5dc3321..8c631ac 100644 --- a/StrokeCog/Supporting Files/Info.plist +++ b/StrokeCog/Supporting Files/Info.plist @@ -2,6 +2,8 @@ + CFBundleAllowMixedLocalizations + ITSAppUsesNonExemptEncryption UIApplicationSceneManifest @@ -11,7 +13,9 @@ UISceneConfigurations - CFBundleAllowMixedLocalizations - + UIBackgroundModes + + location + From 9c9d2bee0f86765645bd4030e70f0d86442a899c Mon Sep 17 00:00:00 2001 From: Vishnu Ravi Date: Tue, 2 Apr 2024 21:43:52 -0400 Subject: [PATCH 03/15] Add location permissions step --- StrokeCog.xcodeproj/project.pbxproj | 4 + StrokeCog/Map/MapboxView.swift | 4 +- .../Onboarding/LocationPermissions.swift | 77 +++++++++++++++++++ StrokeCog/Onboarding/OnboardingFlow.swift | 2 + StrokeCog/Resources/Localizable.xcstrings | 9 +++ 5 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 StrokeCog/Onboarding/LocationPermissions.swift diff --git a/StrokeCog.xcodeproj/project.pbxproj b/StrokeCog.xcodeproj/project.pbxproj index af230df..1bd834e 100644 --- a/StrokeCog.xcodeproj/project.pbxproj +++ b/StrokeCog.xcodeproj/project.pbxproj @@ -76,6 +76,7 @@ 63F4C39B2BBCCCF80033D985 /* LocationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F4C39A2BBCCCF80033D985 /* LocationService.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 */; }; @@ -157,6 +158,7 @@ 63F4C39A2BBCCCF80033D985 /* LocationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationService.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 = ""; }; @@ -260,6 +262,7 @@ 2FE5DC3029EDD7CA004B9AB4 /* HealthKitPermissions.swift */, 2F65B44D2A3B8B0600A36932 /* NotificationPermissions.swift */, 63BBF8152BB8993B006890CE /* StudyIDView.swift */, + 63F4C3A22BBCE79B0033D985 /* LocationPermissions.swift */, ); path = Onboarding; sourceTree = ""; @@ -646,6 +649,7 @@ 63F4C3972BBCCC070033D985 /* MapboxMap.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 /* LocationService.swift in Sources */, diff --git a/StrokeCog/Map/MapboxView.swift b/StrokeCog/Map/MapboxView.swift index 530cabe..17c0694 100644 --- a/StrokeCog/Map/MapboxView.swift +++ b/StrokeCog/Map/MapboxView.swift @@ -5,14 +5,14 @@ // Created by Vishnu Ravi on 4/2/24. // -import SwiftUI import MapboxMaps +import SwiftUI struct MapManagerViewWrapper: UIViewControllerRepresentable { typealias UIViewControllerType = MapManagerView func makeUIViewController(context: Context) -> MapManagerView { - return MapManagerView() + MapManagerView() } func updateUIViewController(_ uiViewController: MapManagerView, context: Context) {} diff --git a/StrokeCog/Onboarding/LocationPermissions.swift b/StrokeCog/Onboarding/LocationPermissions.swift new file mode 100644 index 0000000..006297b --- /dev/null +++ b/StrokeCog/Onboarding/LocationPermissions.swift @@ -0,0 +1,77 @@ +// +// LocationPermissions.swift +// StrokeCog +// +// Created by Vishnu Ravi on 4/2/24. +// + +import SpeziOnboarding +import SpeziScheduler +import SwiftUI + + +struct LocationPermissions: View { + @Environment(OnboardingNavigationPath.self) private var onboardingNavigationPath + @ObservedObject var locationFetcher = LocationService.shared + + @State private var locationProcessing = false + + + var body: some View { + OnboardingView( + contentView: { + VStack { + OnboardingTitleView( + title: "LOCATION_PERMISSIONS_TITLE", + subtitle: "LOCATION_PERMISSIONS_SUBTITLE" + ) + Spacer() + Image(systemName: "bell.square.fill") + .font(.system(size: 150)) + .foregroundColor(.accentColor) + .accessibilityHidden(true) + Text("LOCATION_PERMISSIONS_DESCRIPTION") + .multilineTextAlignment(.center) + .padding(.vertical, 16) + Spacer() + } + }, actionView: { + OnboardingActionsView( + "NOTIFICATION_PERMISSIONS_BUTTON", + action: { + do { + locationProcessing = true + // Notification Authorization is not available in the preview simulator. + if ProcessInfo.processInfo.isPreviewSimulator { + try await _Concurrency.Task.sleep(for: .seconds(5)) + } else { + locationFetcher.requestAuthorizationLocation() + } + } catch { + print("Could not request notification permissions.") + } + locationProcessing = false + + onboardingNavigationPath.nextStep() + } + ) + } + ) + .navigationBarBackButtonHidden(locationProcessing) + // Small fix as otherwise "Login" or "Sign up" is still shown in the nav bar + .navigationTitle(Text(verbatim: "")) + } +} + + +#if DEBUG +#Preview { + OnboardingStack { + NotificationPermissions() + } + .previewWith { + StrokeCogScheduler() + } +} +#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/Resources/Localizable.xcstrings b/StrokeCog/Resources/Localizable.xcstrings index 8244f32..f3afd35 100644 --- a/StrokeCog/Resources/Localizable.xcstrings +++ b/StrokeCog/Resources/Localizable.xcstrings @@ -271,6 +271,15 @@ } } } + }, + "LOCATION_PERMISSIONS_DESCRIPTION" : { + + }, + "LOCATION_PERMISSIONS_SUBTITLE" : { + + }, + "LOCATION_PERMISSIONS_TITLE" : { + }, "Map" : { From 5775de731a74956541a5b2021ee75042422557c0 Mon Sep 17 00:00:00 2001 From: Vishnu Ravi Date: Wed, 3 Apr 2024 06:20:46 -0400 Subject: [PATCH 04/15] Fix linter errors --- StrokeCog/Helper/Date+Helpers.swift | 247 +----------------- StrokeCog/Map/LocationService.swift | 25 +- StrokeCog/Map/LocationUtils.swift | 6 +- StrokeCog/Map/MapboxMap.swift | 38 ++- StrokeCog/Map/MapboxView.swift | 15 +- StrokeCog/Map/StrokeCogMapView.swift | 32 +-- .../Onboarding/LocationPermissions.swift | 7 +- StrokeCog/Resources/Localizable.xcstrings | 3 + StrokeCog/SharedContext/Constants.swift | 3 +- 9 files changed, 63 insertions(+), 313 deletions(-) diff --git a/StrokeCog/Helper/Date+Helpers.swift b/StrokeCog/Helper/Date+Helpers.swift index ffa336d..b95c17b 100644 --- a/StrokeCog/Helper/Date+Helpers.swift +++ b/StrokeCog/Helper/Date+Helpers.swift @@ -1,251 +1,14 @@ // // Date+Helpers.swift -// CS342Support -// -// Created by Santiago Gutierrez on 9/22/19. -// Copyright © 2019 Stanford University. All rights reserved. +// StrokeCog // +// Created by Vishnu Ravi on 4/2/24. import Foundation extension Date { - - public func fullFormattedString() -> String { - return DateFormatter.localizedString(from: self, dateStyle: .full, timeStyle: .none) - } - - public func shortFormattedString() -> String { - return DateFormatter.localizedString(from: self, dateStyle: .short, timeStyle: .none) - } - - public var yesterday: Date { - return dayByAdding(-1) ?? Date().addingTimeInterval(-86400) - } - - public var tomorrow: Date { - return dayByAdding(1) ?? Date().addingTimeInterval(86400) - } - - public var startOfDay: Date { - let cal = Calendar(identifier: Calendar.Identifier.gregorian) - return cal.startOfDay(for: self) - } - - public var endOfDay: Date? { - let calendar = Calendar.current - let components = (calendar as NSCalendar).components([.year, .month, .day], from: self) - - guard let startOfDay = calendar.date(from: components) else { - fatalError("*** Unable to create the start date ***") - } - return (calendar as NSCalendar).date(byAdding: .day, value: 1, to: startOfDay, options: []) - } - - public func ISOStringFromDate() -> String { - let dateFormatter = DateFormatter() - let timeZone = TimeZone(secondsFromGMT: 0) - dateFormatter.locale = Locale(identifier: "en_US_POSIX") - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" - dateFormatter.timeZone = timeZone - - return dateFormatter.string(from: self) + "Z" //this is in UTC, 0 seconds from GMT. - } - - public func shortStringFromDate() -> String { - let dateFormatter = DateFormatter() - let timeZone = TimeZone(secondsFromGMT: 0) - dateFormatter.locale = Locale(identifier: "en_US_POSIX") - dateFormatter.dateFormat = "MM-dd-yyyy" - dateFormatter.timeZone = timeZone - - return dateFormatter.string(from: self) - } - - public func dayByAdding(_ daysToAdd: Int) -> Date? { - let calendar = Calendar.current - let components = (calendar as NSCalendar).components([.year, .month, .day], from: self) - - guard let startOfDay = calendar.date(from: components) else { - fatalError("*** Unable to create the start date ***") - } - return (calendar as NSCalendar).date(byAdding: .day, value: daysToAdd, to: startOfDay, options: []) - } - -} - -extension Date { - fileprivate static func componentFlags() -> NSCalendar.Unit { return [.year, .month, .day, .weekday] } - - static func getTodayHour() -> String { - let calendar = Calendar(identifier: Calendar.Identifier.gregorian) - let components = (calendar as NSCalendar).components([.hour, .minute], from: Date()) - let hour = components.hour ?? 0 - let minutes = components.minute ?? 0 - return "Today, \(hour):\(minutes)" - } - - func getDay() -> Int { - let formatter = DateFormatter() - formatter.dateFormat = "dd-MM-yyyy" - let calendar = Calendar(identifier: Calendar.Identifier.gregorian) - let components = (calendar as NSCalendar).components(Date.componentFlags(), from: self) - let day = components.day - return day! - } - - func getMonth() -> String { - let formatter = DateFormatter() - formatter.dateFormat = "dd-MM-yyyy" - let calendar = Calendar(identifier: Calendar.Identifier.gregorian) - let components = (calendar as NSCalendar).components(Date.componentFlags(), from: self) - let month = formatter.shortMonthSymbols[components.month!-1] - return month - } - - func timeToString() -> String { - return DateFormatter.localizedString(from: self, dateStyle: .none, timeStyle: .short) - } - - func longFormattedString(includeTime: Bool = false) -> String { - return DateFormatter.localizedString(from: self, dateStyle: .long, timeStyle: includeTime ? .long : .none) - } - - static func dateFromComponents(_ components: DateComponents) -> Date? { - let calendar = Calendar(identifier: Calendar.Identifier.gregorian) - return calendar.date(from: components) - } - - static func dateFromString(_ string: String) -> Date? { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss-SSS" - - if let stringDate = dateFormatter.date(from: string) { - return stringDate - } else { - return nil - } - } - - //returns a string with a specific format, conforming to UTC - func stringWithFormat(_ format: String = "yyyy-MM-dd'T'HH:mm:ss.SSS") -> String { - let dateFormatter = DateFormatter() - let timeZone = TimeZone(secondsFromGMT: 0) - dateFormatter.locale = Locale(identifier: "en_US_POSIX") - dateFormatter.dateFormat = format - dateFormatter.timeZone = timeZone - - return dateFormatter.string(from: self) - } - - //returns a string with ISO format conforming to local timezone. - func localStringFromDate() -> String { - let dateFormatter = DateFormatter() - dateFormatter.locale = Locale(identifier: "en_US_POSIX") - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" - dateFormatter.timeZone = TimeZone.current - - return dateFormatter.string(from: self) - } - - static func dateFromISOString(_ string: String) -> Date? { - let dateFormatter = DateFormatter() - dateFormatter.locale = Locale(identifier: "en_US_POSIX") - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" - //dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" - - - if let stringDate = dateFormatter.date(from: string) { - return stringDate - } else { - return nil - } - } - - public func isLessThanOneMinute(_ dateToCompare: Date) -> Bool { - if abs(self.minutesFrom(dateToCompare)) < 1 { - return true - } - return false - } - - func minutesFrom(_ dateToCompare: Date) -> Int { - return (Calendar.current as NSCalendar).components([.minute], from: dateToCompare, to: self, options: []).minute! - } - - func daysTo(_ dateToCompare: Date) -> Int { - return (Calendar.current as NSCalendar).components([.day], from: self, to: dateToCompare, options: []).day! - } - - func monthByAdding(_ monthsToAdd: Int) -> Date? { - let calendar = Calendar.current - return (calendar as NSCalendar).date(byAdding: .month, value: monthsToAdd, to: self, options: []) - } - - func startOfMonth() -> Date { - let calendar = Calendar.current; - let components = (calendar as NSCalendar).components([.year, .month], from: self); - - guard let startOfMonth = calendar.date(from: components) else { - fatalError("*** Unable to create the start date ***") - } - - return startOfMonth; - } - - func endOfMonth() -> Date? { - let calendar = Calendar.current - let components = (calendar as NSCalendar).components([.year, .month], from: self) - - guard let startOfMonth = calendar.date(from: components) else { - fatalError("*** Unable to create the start date ***") - } - return (calendar as NSCalendar).date(byAdding: .month, value: 1, to: startOfMonth, options: []) - } -} - -extension Date { - /// Returns the amount of years from another date - func years(from date: Date) -> Int { - return Calendar.current.dateComponents([.year], from: date, to: self).year ?? 0 - } - /// Returns the amount of months from another date - func months(from date: Date) -> Int { - return Calendar.current.dateComponents([.month], from: date, to: self).month ?? 0 - } - /// Returns the amount of weeks from another date - func weeks(from date: Date) -> Int { - return Calendar.current.dateComponents([.weekOfMonth], from: date, to: self).weekOfMonth ?? 0 - } - /// Returns the amount of days from another date - func days(from date: Date) -> Int { - return Calendar.current.dateComponents([.day], from: date, to: self).day ?? 0 - } - /// Returns the amount of hours from another date - func hours(from date: Date) -> Int { - return Calendar.current.dateComponents([.hour], from: date, to: self).hour ?? 0 - } - /// Returns the amount of minutes from another date - func minutes(from date: Date) -> Int { - return Calendar.current.dateComponents([.minute], from: date, to: self).minute ?? 0 - } - /// Returns the amount of seconds from another date - func seconds(from date: Date) -> Int { - return Calendar.current.dateComponents([.second], from: date, to: self).second ?? 0 - } - /// Returns the amount of nanoseconds from another date - func nanoseconds(from date: Date) -> Int { - return Calendar.current.dateComponents([.nanosecond], from: date, to: self).nanosecond ?? 0 - } - /// Returns the a custom time interval description from another date - func offset(from date: Date) -> String { - if years(from: date) > 0 { return "\(years(from: date))y" } - if months(from: date) > 0 { return "\(months(from: date))M" } - if weeks(from: date) > 0 { return "\(weeks(from: date))w" } - if days(from: date) > 0 { return "\(days(from: date))d" } - if hours(from: date) > 0 { return "\(hours(from: date))h" } - if minutes(from: date) > 0 { return "\(minutes(from: date))m" } - if seconds(from: date) > 0 { return "\(seconds(from: date))s" } - if nanoseconds(from: date) > 0 { return "\(nanoseconds(from: date))ns" } - return "" + /// Returns the start of the day for the Date + var startOfDay: Date { + Calendar.current.startOfDay(for: self) } } diff --git a/StrokeCog/Map/LocationService.swift b/StrokeCog/Map/LocationService.swift index d6139fd..bde94c9 100644 --- a/StrokeCog/Map/LocationService.swift +++ b/StrokeCog/Map/LocationService.swift @@ -8,7 +8,7 @@ import CoreLocation import Firebase import Foundation -class LocationService: NSObject, CLLocationManagerDelegate, ObservableObject { +public class LocationService: NSObject, CLLocationManagerDelegate, ObservableObject { static let shared = LocationService() private let manager = CLLocationManager() @@ -48,7 +48,7 @@ class LocationService: NSObject, CLLocationManagerDelegate, ObservableObject { } } - func startTracking() { + public func startTracking() { if CLLocationManager.locationServicesEnabled() { self.manager.startUpdatingLocation() self.manager.startMonitoringSignificantLocationChanges() @@ -61,13 +61,13 @@ class LocationService: NSObject, CLLocationManagerDelegate, ObservableObject { } } - func stopTracking() { + public func stopTracking() { self.manager.stopUpdatingLocation() self.manager.stopMonitoringSignificantLocationChanges() print("[LIFESPACE] Stopping tracking...") } - func calculateIfCanShowRequestMessage() { + public func calculateIfCanShowRequestMessage() { let previousState = authorizationStatus authorizationStatus = self.manager.authorizationStatus @@ -91,21 +91,21 @@ class LocationService: NSObject, CLLocationManagerDelegate, ObservableObject { } } - func requestAuthorizationLocation() { + 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()) { + func fetchPoints(date: Date = Date()) { // JHMapDataManager.shared.getAllMapPoints(date: date, onCompletion: {(results) in // if let results = results as? [CLLocationCoordinate2D] { // self.allLocations = results // self.onLocationsUpdated?(self.allLocations) // } // }) -// } + } /// Adds a new point to the map and saves the location to the database, /// if it meets the criteria to be added. @@ -115,7 +115,6 @@ class LocationService: NSObject, CLLocationManagerDelegate, ObservableObject { 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, @@ -125,7 +124,7 @@ class LocationService: NSObject, CLLocationManagerDelegate, ObservableObject { // Reset all points when day changes if Date().startOfDay != previousDate.startOfDay { add = true - //fetchPoints() + fetchPoints() } } @@ -161,11 +160,11 @@ class LocationService: NSObject, CLLocationManagerDelegate, ObservableObject { } } - func userAuthorizeAlways() -> Bool { - return self.manager.authorizationStatus == .authorizedAlways + public func userAuthorizeAlways() -> Bool { + self.manager.authorizationStatus == .authorizedAlways } - func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + 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 @@ -174,7 +173,7 @@ class LocationService: NSObject, CLLocationManagerDelegate, ObservableObject { lastKnownLocation = locations.first?.coordinate } - func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { self.calculateIfCanShowRequestMessage() } } diff --git a/StrokeCog/Map/LocationUtils.swift b/StrokeCog/Map/LocationUtils.swift index 8fef0b0..a66da81 100644 --- a/StrokeCog/Map/LocationUtils.swift +++ b/StrokeCog/Map/LocationUtils.swift @@ -3,19 +3,19 @@ // LifeSpace // // Created by Vishnu Ravi on 7/12/22. -// Copyright © 2022 CocoaPods. All rights reserved. +// Copyright © 2022 LifeSpace. All rights reserved. // import CoreLocation import Foundation -class LocationUtils { +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 - public static func isAboveMinimumDistance( + static func isAboveMinimumDistance( previousLocation: CLLocationCoordinate2D, currentLocation: CLLocationCoordinate2D ) -> Bool { diff --git a/StrokeCog/Map/MapboxMap.swift b/StrokeCog/Map/MapboxMap.swift index 6095874..1ee31b3 100644 --- a/StrokeCog/Map/MapboxMap.swift +++ b/StrokeCog/Map/MapboxMap.swift @@ -9,22 +9,22 @@ import Foundation import MapboxMaps // swiftlint:disable closure_body_length -class MapboxMap { - public static func initializeMap (mapView: MapView, reload: Bool) { +enum MapboxMap { + static func initializeMap (mapView: MapView, reload: Bool) { mapView.mapboxMap.onNext(event: .mapLoaded) { _ in var locationsPoints = [CLLocationCoordinate2D]() locationsPoints = LocationService.shared.allLocations do { var source = GeoJSONSource(id: "GEOSOURCE") source.data = .feature(Feature(geometry: .multiPoint(MultiPoint(locationsPoints)))) - try mapView.mapboxMap.style.addSource(source) - + try mapView.mapboxMap.addSource(source) + var circlesLayer = CircleLayer(id: "CIRCLELAYER", source: "GEOSOURCE") circlesLayer.circleColor = .constant(StyleColor(.red)) circlesLayer.circleStrokeColor = .constant(StyleColor(.black)) circlesLayer.circleStrokeWidth = .constant(2) - try mapView.mapboxMap.style.addLayer(circlesLayer, layerPosition: .above("country-label")) - + try mapView.mapboxMap.addLayer(circlesLayer, layerPosition: .above("country-label")) + mapView.mapboxMap.setCamera( to: CameraOptions( center: LocationService.shared.allLocations.last, @@ -33,24 +33,20 @@ class MapboxMap { ) if reload { LocationService.shared.onLocationsUpdated = { locations in - do { - try mapView.mapboxMap.style.updateGeoJSONSource( - withId: "GEOSOURCE", - geoJSON: .feature( - Feature( - geometry: .lineString(LineString(locations)) - ) + mapView.mapboxMap.updateGeoJSONSource( + withId: "GEOSOURCE", + geoJSON: .feature( + Feature( + geometry: .lineString(LineString(locations)) ) ) - mapView.mapboxMap.setCamera( - to: CameraOptions( - center: LocationService.shared.allLocations.last, - zoom: 14.0 - ) + ) + mapView.mapboxMap.setCamera( + to: CameraOptions( + center: LocationService.shared.allLocations.last, + zoom: 14.0 ) - } catch let error as NSError { - print("[LIFESPACE] Error updating map: \(error.localizedDescription)") - } + ) } } } catch let error as NSError { diff --git a/StrokeCog/Map/MapboxView.swift b/StrokeCog/Map/MapboxView.swift index 17c0694..5f75817 100644 --- a/StrokeCog/Map/MapboxView.swift +++ b/StrokeCog/Map/MapboxView.swift @@ -18,16 +18,17 @@ struct MapManagerViewWrapper: UIViewControllerRepresentable { func updateUIViewController(_ uiViewController: MapManagerView, context: Context) {} } -class MapManagerView: UIViewController { - internal var mapView: MapView! +public class MapManagerView: UIViewController { + internal lazy var mapView: MapView = { + let map = MapView(frame: view.bounds) + map.autoresizingMask = [.flexibleWidth, .flexibleHeight] + MapboxMap.initializeMap(mapView: map, reload: true) + map.location.options.puckType = .puck2D() + return map + }() override public func viewDidLoad() { super.viewDidLoad() - - mapView = MapView(frame: view.bounds) - mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.view.addSubview(mapView) - MapboxMap.initializeMap(mapView: mapView, reload: true) - mapView.location.options.puckType = .puck2D() } } diff --git a/StrokeCog/Map/StrokeCogMapView.swift b/StrokeCog/Map/StrokeCogMapView.swift index 94d7383..9cff167 100644 --- a/StrokeCog/Map/StrokeCogMapView.swift +++ b/StrokeCog/Map/StrokeCogMapView.swift @@ -1,45 +1,35 @@ // // HomeView.swift -// LifeSpace +// StrokeCog // -// Created by Vishnu Ravi on 5/18/22. -// Copyright © 2022 LifeSpace. All rights reserved. +// Created by Vishnu Ravi on 4/2/24. +// Copyright © 2024 StrokeCog. All rights reserved. // -import SwiftUI @_spi(Experimental) import MapboxMaps +import SwiftUI -// swiftlint:disable closure_body_length file_types_order struct StrokeCogMapView: View { + @AppStorage(StorageKeys.trackingPreference) private var trackingOn = true + @State private var showingSurveyAlert = false @State private var alertMessage = "" @State private var showingSurvey = false - @AppStorage(StorageKeys.trackingPreference) private var trackingOn = true @State private var optionsPanelOpen = true - init() { - MapboxOptions.accessToken = "" - } - var body: some View { ZStack { MapManagerViewWrapper() } } -} - -struct ButtonGroupBoxStyle: GroupBoxStyle { - func makeBody(configuration: Configuration) -> some View { - configuration.content - .frame(maxWidth: .infinity) - .padding() - .background(RoundedRectangle(cornerRadius: 8).fill(Color("primaryRed"))) + + init() { + MapboxOptions.accessToken = "" } } -struct HomeView_Previews: PreviewProvider { +struct StrokeCogMapView_Previews: PreviewProvider { static var previews: some View { - HomeView() + StrokeCogMapView() } } - diff --git a/StrokeCog/Onboarding/LocationPermissions.swift b/StrokeCog/Onboarding/LocationPermissions.swift index 006297b..25cbfda 100644 --- a/StrokeCog/Onboarding/LocationPermissions.swift +++ b/StrokeCog/Onboarding/LocationPermissions.swift @@ -12,7 +12,7 @@ import SwiftUI struct LocationPermissions: View { @Environment(OnboardingNavigationPath.self) private var onboardingNavigationPath - @ObservedObject var locationFetcher = LocationService.shared + @ObservedObject var locationService = LocationService.shared @State private var locationProcessing = false @@ -37,7 +37,7 @@ struct LocationPermissions: View { } }, actionView: { OnboardingActionsView( - "NOTIFICATION_PERMISSIONS_BUTTON", + "LOCATION_PERMISSIONS_BUTTON", action: { do { locationProcessing = true @@ -45,7 +45,7 @@ struct LocationPermissions: View { if ProcessInfo.processInfo.isPreviewSimulator { try await _Concurrency.Task.sleep(for: .seconds(5)) } else { - locationFetcher.requestAuthorizationLocation() + locationService.requestAuthorizationLocation() } } catch { print("Could not request notification permissions.") @@ -74,4 +74,3 @@ struct LocationPermissions: View { } } #endif - diff --git a/StrokeCog/Resources/Localizable.xcstrings b/StrokeCog/Resources/Localizable.xcstrings index f3afd35..0060770 100644 --- a/StrokeCog/Resources/Localizable.xcstrings +++ b/StrokeCog/Resources/Localizable.xcstrings @@ -271,6 +271,9 @@ } } } + }, + "LOCATION_PERMISSIONS_BUTTON" : { + }, "LOCATION_PERMISSIONS_DESCRIPTION" : { diff --git a/StrokeCog/SharedContext/Constants.swift b/StrokeCog/SharedContext/Constants.swift index f194522..e444e66 100644 --- a/StrokeCog/SharedContext/Constants.swift +++ b/StrokeCog/SharedContext/Constants.swift @@ -5,8 +5,7 @@ // Created by Vishnu Ravi on 4/2/24. // -class Constants { - +enum Constants { static let prefConfirmedLogin = "PREF_CONFIRMED_LOGIN" static let prefFirstRunWasMarked = "PREF_FIRST_RUN" static let prefUserEmail = "PREF_USER_EMAIL" From a17033e285f7fa3697359ea2483edf0ae706855a Mon Sep 17 00:00:00 2001 From: Vishnu Ravi Date: Wed, 3 Apr 2024 06:33:20 -0400 Subject: [PATCH 05/15] Update build and test action --- .github/workflows/build-and-test.yml | 31 ++++++++++++------- .../Onboarding/LocationPermissions.swift | 5 +-- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 1591d59..dc8cea3 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -24,17 +24,26 @@ jobs: 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'" + build: + name: Build + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + - name: Check Environment + run: | + xcodebuild -version + swift --version + - name: Set Mapbox Credentials + run: | + echo "machine api.mapbox.com" >> ~/.netrc + echo "login mapbox" >> ~/.netrc + echo "password ${{ secrets.MAPBOX_TOKEN }}" >> ~/.netrc + chmod 0600 ~/.netrc + - name: Build + run: xcodebuild test -project StrokeCog.xcodeproj -scheme StrokeCog -destination 'platform=iOS Simulator,name=iPhone 15 Pro' uploadcoveragereport: name: Upload Coverage Report needs: buildandtest diff --git a/StrokeCog/Onboarding/LocationPermissions.swift b/StrokeCog/Onboarding/LocationPermissions.swift index 25cbfda..54e01a0 100644 --- a/StrokeCog/Onboarding/LocationPermissions.swift +++ b/StrokeCog/Onboarding/LocationPermissions.swift @@ -67,10 +67,7 @@ struct LocationPermissions: View { #if DEBUG #Preview { OnboardingStack { - NotificationPermissions() + LocationPermissions() } - .previewWith { - StrokeCogScheduler() - } } #endif From 2e714cdc0ab1494ec3b5bbb1208fda619e292884 Mon Sep 17 00:00:00 2001 From: Vishnu Ravi Date: Wed, 3 Apr 2024 06:33:41 -0400 Subject: [PATCH 06/15] Update build and test action --- .github/workflows/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index dc8cea3..3f5b710 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -26,7 +26,7 @@ jobs: contents: read build: name: Build - runs-on: macos-latest + runs-on: [macos-latest, self-hosted] steps: - uses: actions/checkout@v2 - uses: maxim-lobanov/setup-xcode@v1 From 0166dfe42622073155bf711d0d6fe06bd0488be6 Mon Sep 17 00:00:00 2001 From: Vishnu Ravi Date: Wed, 3 Apr 2024 06:34:37 -0400 Subject: [PATCH 07/15] Fix build action --- .github/workflows/build-and-test.yml | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 3f5b710..d7b6eb6 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -24,7 +24,7 @@ jobs: uses: StanfordBDHG/.github/.github/workflows/markdown-link-check.yml@v2 permissions: contents: read - build: + buildandtest: name: Build runs-on: [macos-latest, self-hosted] steps: @@ -43,14 +43,4 @@ jobs: echo "password ${{ secrets.MAPBOX_TOKEN }}" >> ~/.netrc chmod 0600 ~/.netrc - name: Build - run: xcodebuild test -project StrokeCog.xcodeproj -scheme StrokeCog -destination 'platform=iOS Simulator,name=iPhone 15 Pro' - 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 }} + run: xcodebuild test -project StrokeCog.xcodeproj -scheme StrokeCog -destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ No newline at end of file From 303d9f859a33dfdf560fba7f62795cf49e596821 Mon Sep 17 00:00:00 2001 From: Vishnu Ravi Date: Wed, 3 Apr 2024 10:23:23 -0400 Subject: [PATCH 08/15] Update mapbox manager --- .github/workflows/build-and-test.yml | 22 +---- StrokeCog.xcodeproj/project.pbxproj | 22 +++-- .../LocationModule.swift} | 9 +- .../{Map => Location}/LocationUtils.swift | 0 StrokeCog/Map/MapboxMap.swift | 57 ------------ StrokeCog/Map/MapboxView.swift | 92 ++++++++++++++++++- .../Onboarding/LocationPermissions.swift | 6 +- StrokeCog/Resources/Localizable.xcstrings | 36 +++++++- StrokeCog/StrokeCogDelegate.swift | 1 + 9 files changed, 142 insertions(+), 103 deletions(-) rename StrokeCog/{Map/LocationService.swift => Location/LocationModule.swift} (96%) rename StrokeCog/{Map => Location}/LocationUtils.swift (100%) delete mode 100644 StrokeCog/Map/MapboxMap.swift diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index d7b6eb6..9c2063e 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -23,24 +23,4 @@ jobs: name: Markdown Link Check uses: StanfordBDHG/.github/.github/workflows/markdown-link-check.yml@v2 permissions: - contents: read - buildandtest: - name: Build - runs-on: [macos-latest, self-hosted] - steps: - - uses: actions/checkout@v2 - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: latest-stable - - name: Check Environment - run: | - xcodebuild -version - swift --version - - name: Set Mapbox Credentials - run: | - echo "machine api.mapbox.com" >> ~/.netrc - echo "login mapbox" >> ~/.netrc - echo "password ${{ secrets.MAPBOX_TOKEN }}" >> ~/.netrc - chmod 0600 ~/.netrc - - name: Build - run: xcodebuild test -project StrokeCog.xcodeproj -scheme StrokeCog -destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ No newline at end of file + contents: read \ No newline at end of file diff --git a/StrokeCog.xcodeproj/project.pbxproj b/StrokeCog.xcodeproj/project.pbxproj index 1bd834e..0c59537 100644 --- a/StrokeCog.xcodeproj/project.pbxproj +++ b/StrokeCog.xcodeproj/project.pbxproj @@ -71,9 +71,8 @@ 6347EB742BBBF442008E0C4A /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6347EB732BBBF442008E0C4A /* Constants.swift */; }; 63BBF8162BB8993B006890CE /* StudyIDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63BBF8152BB8993B006890CE /* StudyIDView.swift */; }; 63BBF8192BB89CF7006890CE /* studyIDs.csv in Resources */ = {isa = PBXBuildFile; fileRef = 63BBF8182BB89CF7006890CE /* studyIDs.csv */; }; - 63F4C3972BBCCC070033D985 /* MapboxMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F4C3962BBCCC070033D985 /* MapboxMap.swift */; }; 63F4C3992BBCCC300033D985 /* MapboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F4C3982BBCCC300033D985 /* MapboxView.swift */; }; - 63F4C39B2BBCCCF80033D985 /* LocationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F4C39A2BBCCCF80033D985 /* LocationService.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 */; }; @@ -153,9 +152,8 @@ 6347EB732BBBF442008E0C4A /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.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 = ""; }; - 63F4C3962BBCCC070033D985 /* MapboxMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapboxMap.swift; sourceTree = ""; }; 63F4C3982BBCCC300033D985 /* MapboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapboxView.swift; sourceTree = ""; }; - 63F4C39A2BBCCCF80033D985 /* LocationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationService.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 = ""; }; @@ -329,14 +327,20 @@ isa = PBXGroup; children = ( 6347EB632BBBA895008E0C4A /* StrokeCogMapView.swift */, - 63F4C3962BBCCC070033D985 /* MapboxMap.swift */, 63F4C3982BBCCC300033D985 /* MapboxView.swift */, - 63F4C39A2BBCCCF80033D985 /* LocationService.swift */, - 63F4C39C2BBCCD200033D985 /* LocationUtils.swift */, ); path = Map; sourceTree = ""; }; + 637AA5CF2BBDA686007BD7A3 /* Location */ = { + isa = PBXGroup; + children = ( + 63F4C39A2BBCCCF80033D985 /* LocationModule.swift */, + 63F4C39C2BBCCD200033D985 /* LocationUtils.swift */, + ); + path = Location; + sourceTree = ""; + }; 653A2544283387FE005D4D48 = { isa = PBXGroup; children = ( @@ -372,6 +376,7 @@ 2FE5DC3B29EDD7D0004B9AB4 /* Schedule */, 2FE5DC2729EDD38D004B9AB4 /* Contacts */, 56F6F29E2AB441640022FE5A /* Contributions */, + 637AA5CF2BBDA686007BD7A3 /* Location */, 6347EB622BBBA874008E0C4A /* Map */, 2F4FC8D529EE69BE00BFFE26 /* MockUpload */, 2FE5DC3C29EDD7DA004B9AB4 /* SharedContext */, @@ -646,13 +651,12 @@ 2FE5DC4629EDD7F2004B9AB4 /* Bundle+Image.swift in Sources */, 2FE5DC4F29EDD7FA004B9AB4 /* EventContext.swift in Sources */, 6347EB642BBBA895008E0C4A /* StrokeCogMapView.swift in Sources */, - 63F4C3972BBCCC070033D985 /* MapboxMap.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 /* LocationService.swift in Sources */, + 63F4C39B2BBCCCF80033D985 /* LocationModule.swift in Sources */, 6347EB742BBBF442008E0C4A /* Constants.swift in Sources */, 63F4C39D2BBCCD200033D985 /* LocationUtils.swift in Sources */, 2FE5DC5329EDD7FA004B9AB4 /* Bundle+Questionnaire.swift in Sources */, diff --git a/StrokeCog/Map/LocationService.swift b/StrokeCog/Location/LocationModule.swift similarity index 96% rename from StrokeCog/Map/LocationService.swift rename to StrokeCog/Location/LocationModule.swift index bde94c9..594adab 100644 --- a/StrokeCog/Map/LocationService.swift +++ b/StrokeCog/Location/LocationModule.swift @@ -7,11 +7,10 @@ import CoreLocation import Firebase import Foundation +import Spezi -public class LocationService: NSObject, CLLocationManagerDelegate, ObservableObject { - static let shared = LocationService() - - private let manager = CLLocationManager() +public class LocationModule: NSObject, CLLocationManagerDelegate, Module, DefaultInitializable, EnvironmentAccessible { + private(set) var manager = CLLocationManager() public var allLocations = [CLLocationCoordinate2D]() public var onLocationsUpdated: (([CLLocationCoordinate2D]) -> Void)? @@ -31,7 +30,7 @@ public class LocationService: NSObject, CLLocationManagerDelegate, ObservableObj } } - override init() { + required public override init() { super.init() manager.delegate = self diff --git a/StrokeCog/Map/LocationUtils.swift b/StrokeCog/Location/LocationUtils.swift similarity index 100% rename from StrokeCog/Map/LocationUtils.swift rename to StrokeCog/Location/LocationUtils.swift diff --git a/StrokeCog/Map/MapboxMap.swift b/StrokeCog/Map/MapboxMap.swift deleted file mode 100644 index 1ee31b3..0000000 --- a/StrokeCog/Map/MapboxMap.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// MapboxMap.swift -// StrokeCog -// -// Created by Vishnu Ravi on 4/2/24. -// - -import Foundation -import MapboxMaps - -// swiftlint:disable closure_body_length -enum MapboxMap { - static func initializeMap (mapView: MapView, reload: Bool) { - mapView.mapboxMap.onNext(event: .mapLoaded) { _ in - var locationsPoints = [CLLocationCoordinate2D]() - locationsPoints = LocationService.shared.allLocations - do { - var source = GeoJSONSource(id: "GEOSOURCE") - source.data = .feature(Feature(geometry: .multiPoint(MultiPoint(locationsPoints)))) - try mapView.mapboxMap.addSource(source) - - var circlesLayer = CircleLayer(id: "CIRCLELAYER", source: "GEOSOURCE") - circlesLayer.circleColor = .constant(StyleColor(.red)) - circlesLayer.circleStrokeColor = .constant(StyleColor(.black)) - circlesLayer.circleStrokeWidth = .constant(2) - try mapView.mapboxMap.addLayer(circlesLayer, layerPosition: .above("country-label")) - - mapView.mapboxMap.setCamera( - to: CameraOptions( - center: LocationService.shared.allLocations.last, - zoom: 14.0 - ) - ) - if reload { - LocationService.shared.onLocationsUpdated = { locations in - mapView.mapboxMap.updateGeoJSONSource( - withId: "GEOSOURCE", - geoJSON: .feature( - Feature( - geometry: .lineString(LineString(locations)) - ) - ) - ) - mapView.mapboxMap.setCamera( - to: CameraOptions( - center: LocationService.shared.allLocations.last, - zoom: 14.0 - ) - ) - } - } - } catch let error as NSError { - print("[LIFESPACE] Error adding source or layer: \(error.localizedDescription)") - } - } - } -} diff --git a/StrokeCog/Map/MapboxView.swift b/StrokeCog/Map/MapboxView.swift index 5f75817..52f6cd0 100644 --- a/StrokeCog/Map/MapboxView.swift +++ b/StrokeCog/Map/MapboxView.swift @@ -6,29 +6,113 @@ // import MapboxMaps +import Spezi import SwiftUI struct MapManagerViewWrapper: UIViewControllerRepresentable { + @Environment(LocationModule.self) private var locationModule typealias UIViewControllerType = MapManagerView func makeUIViewController(context: Context) -> MapManagerView { - MapManagerView() + MapManagerView(locationModule: locationModule) } func updateUIViewController(_ uiViewController: MapManagerView, context: Context) {} } public class MapManagerView: UIViewController { - internal lazy var mapView: MapView = { + private var locationModule: LocationModule? + + private enum Constants { + static let geoSourceId = "GEOSOURCE" + static let circleLayerId = "CIRCLELAYER" + static let zoomLevel: Double = 14.0 + static let countryLabelLayerId = "country-label" + } + + private lazy var mapView: MapView = { let map = MapView(frame: view.bounds) map.autoresizingMask = [.flexibleWidth, .flexibleHeight] - MapboxMap.initializeMap(mapView: map, reload: true) 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) + } + + 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/Onboarding/LocationPermissions.swift b/StrokeCog/Onboarding/LocationPermissions.swift index 54e01a0..049ba90 100644 --- a/StrokeCog/Onboarding/LocationPermissions.swift +++ b/StrokeCog/Onboarding/LocationPermissions.swift @@ -12,7 +12,7 @@ import SwiftUI struct LocationPermissions: View { @Environment(OnboardingNavigationPath.self) private var onboardingNavigationPath - @ObservedObject var locationService = LocationService.shared + @Environment(LocationModule.self) private var locationModule @State private var locationProcessing = false @@ -41,11 +41,11 @@ struct LocationPermissions: View { action: { do { locationProcessing = true - // Notification Authorization is not available in the preview simulator. + // Location authorization is not available in the preview simulator. if ProcessInfo.processInfo.isPreviewSimulator { try await _Concurrency.Task.sleep(for: .seconds(5)) } else { - locationService.requestAuthorizationLocation() + locationModule.requestAuthorizationLocation() } } catch { print("Could not request notification permissions.") diff --git a/StrokeCog/Resources/Localizable.xcstrings b/StrokeCog/Resources/Localizable.xcstrings index 0060770..9e1ab78 100644 --- a/StrokeCog/Resources/Localizable.xcstrings +++ b/StrokeCog/Resources/Localizable.xcstrings @@ -273,16 +273,44 @@ } }, "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" : { 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() } } From 9ff451bfda62c6ff1b99ff102c19fb8c1b4eff13 Mon Sep 17 00:00:00 2001 From: Vishnu Ravi Date: Wed, 3 Apr 2024 11:47:41 -0400 Subject: [PATCH 09/15] Add build action on GitHub runners --- .github/workflows/build-and-test.yml | 22 ++++++++++++++++++- .../xcshareddata/swiftpm/Package.resolved | 2 +- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 9c2063e..2b76bce 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -23,4 +23,24 @@ jobs: name: Markdown Link Check uses: StanfordBDHG/.github/.github/workflows/markdown-link-check.yml@v2 permissions: - contents: read \ No newline at end of file + contents: read + buildandtest: + name: Build and Test + runs-on: macos-13 + steps: + - uses: actions/checkout@v2 + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + - name: Check Environment + run: | + xcodebuild -version + swift --version + - name: Set Mapbox Credentials + run: | + echo "machine api.mapbox.com" >> ~/.netrc + echo "login mapbox" >> ~/.netrc + echo "password ${{ secrets.MAPBOX_TOKEN }}" >> ~/.netrc + chmod 0600 ~/.netrc + - name: Build and Test + run: xcodebuild test -project StrokeCog.xcodeproj -scheme StrokeCog -destination 'platform=iOS Simulator,name=iPhone 15' \ No newline at end of file diff --git a/StrokeCog.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/StrokeCog.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 94508df..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" : "43e89b81bc5041bc458ff8ef663965df68e755e6e2fdcb3a11c344d1cc5c0249", + "originHash" : "743bf7cd89ddbade5c78d5f9c6554803b47ff2ebf7a758e70d2c9c4b9167c70f", "pins" : [ { "identity" : "abseil-cpp-binary", From d0275d18689f472e25f0e3d9a9e9eb219a2f6fe4 Mon Sep 17 00:00:00 2001 From: Vishnu Ravi Date: Wed, 3 Apr 2024 11:56:33 -0400 Subject: [PATCH 10/15] Disable build action --- .github/workflows/build-and-test.yml | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 2b76bce..9c2063e 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -23,24 +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 - runs-on: macos-13 - steps: - - uses: actions/checkout@v2 - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: latest-stable - - name: Check Environment - run: | - xcodebuild -version - swift --version - - name: Set Mapbox Credentials - run: | - echo "machine api.mapbox.com" >> ~/.netrc - echo "login mapbox" >> ~/.netrc - echo "password ${{ secrets.MAPBOX_TOKEN }}" >> ~/.netrc - chmod 0600 ~/.netrc - - name: Build and Test - run: xcodebuild test -project StrokeCog.xcodeproj -scheme StrokeCog -destination 'platform=iOS Simulator,name=iPhone 15' \ No newline at end of file + contents: read \ No newline at end of file From 8292004979d9aafbe7dda22e5aa01e89658dd9fb Mon Sep 17 00:00:00 2001 From: Vishnu Ravi Date: Wed, 3 Apr 2024 12:48:05 -0400 Subject: [PATCH 11/15] Minor updates --- StrokeCog/Location/LocationModule.swift | 64 +++-------------------- StrokeCog/Map/MapboxView.swift | 9 ++-- StrokeCog/Map/StrokeCogMapView.swift | 6 +-- StrokeCog/Onboarding/StudyIDView.swift | 10 ++-- StrokeCog/Resources/Localizable.xcstrings | 61 ++++++++++++++++----- StrokeCog/Supporting Files/Info.plist | 4 ++ 6 files changed, 70 insertions(+), 84 deletions(-) diff --git a/StrokeCog/Location/LocationModule.swift b/StrokeCog/Location/LocationModule.swift index 594adab..652f383 100644 --- a/StrokeCog/Location/LocationModule.swift +++ b/StrokeCog/Location/LocationModule.swift @@ -11,7 +11,6 @@ import Spezi public class LocationModule: NSObject, CLLocationManagerDelegate, Module, DefaultInitializable, EnvironmentAccessible { private(set) var manager = CLLocationManager() - public var allLocations = [CLLocationCoordinate2D]() public var onLocationsUpdated: (([CLLocationCoordinate2D]) -> Void)? @@ -30,12 +29,10 @@ public class LocationModule: NSObject, CLLocationManagerDelegate, Module, Defaul } } - required public override init() { + override public required init() { super.init() manager.delegate = self - calculateIfCanShowRequestMessage() - // 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) @@ -45,6 +42,9 @@ public class LocationModule: NSObject, CLLocationManagerDelegate, Module, Defaul if UserDefaults.standard.bool(forKey: Constants.prefTrackingStatus) { self.startTracking() } + + // Disable Mapbox telemetry + UserDefaults.standard.set(false, forKey: "MGLMapboxMetricsEnabled") } public func startTracking() { @@ -66,30 +66,6 @@ public class LocationModule: NSObject, CLLocationManagerDelegate, Module, Defaul print("[LIFESPACE] Stopping tracking...") } - public func calculateIfCanShowRequestMessage() { - let previousState = authorizationStatus - authorizationStatus = self.manager.authorizationStatus - - let first = UserDefaults.standard.bool(forKey: Constants.JHFirstLocationRequest) - - if authorizationStatus == .authorizedWhenInUse && !first && previousState != authorizationStatus { - UserDefaults.standard.set(true, forKey: Constants.JHFirstLocationRequest) - } else { - UserDefaults.standard.set(false, forKey: Constants.JHFirstLocationRequest) - } - - if authorizationStatus == .authorizedAlways { - // LaunchModel.sharedinstance.showPermissionView = false - } - - if self.manager.authorizationStatus == .notDetermined || - (self.manager.authorizationStatus == .authorizedWhenInUse && UserDefaults.standard.bool(forKey: Constants.JHFirstLocationRequest)) { - canShowRequestMessage = true - } else { - canShowRequestMessage = false - } - } - public func requestAuthorizationLocation() { self.manager.requestWhenInUseAuthorization() self.manager.requestAlwaysAuthorization() @@ -98,12 +74,7 @@ public class LocationModule: NSObject, CLLocationManagerDelegate, Module, Defaul /// 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()) { -// JHMapDataManager.shared.getAllMapPoints(date: date, onCompletion: {(results) in -// if let results = results as? [CLLocationCoordinate2D] { -// self.allLocations = results -// self.onLocationsUpdated?(self.allLocations) -// } -// }) + // TODO: Fetch from Firestore } /// Adds a new point to the map and saves the location to the database, @@ -134,28 +105,7 @@ public class LocationModule: NSObject, CLLocationManagerDelegate, Module, Defaul previousLocation = point previousDate = Date() -// // write this location to the database -// if let mapPointsCollection = CKStudyUser.shared.mapPointsCollection, -// let user = CKStudyUser.shared.currentUser, -// let studyID = CKStudyUser.shared.studyID { -// let db = Firestore.firestore() -// db.collection(mapPointsCollection) -// .document(UUID().uuidString) -// .setData([ -// "currentdate": NSDate(), -// "time": NSDate().timeIntervalSince1970, -// "latitude": point.latitude, -// "longitude": point.longitude, -// "studyID": studyID, -// "UpdatedBy": user.uid -// ]) { err in -// if let err = err { -// print("[LIFESPACE] Error writing location to database: \(err)") -// } -// } -// } else { -// print("[LIFESPACE] Unable to save point due to missing metadata.") -// } + // TODO: Save to Firestore } } @@ -173,6 +123,6 @@ public class LocationModule: NSObject, CLLocationManagerDelegate, Module, Defaul } public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { - self.calculateIfCanShowRequestMessage() + // TODO: Handle authorization change } } diff --git a/StrokeCog/Map/MapboxView.swift b/StrokeCog/Map/MapboxView.swift index 52f6cd0..a45747b 100644 --- a/StrokeCog/Map/MapboxView.swift +++ b/StrokeCog/Map/MapboxView.swift @@ -10,9 +10,10 @@ import Spezi import SwiftUI struct MapManagerViewWrapper: UIViewControllerRepresentable { - @Environment(LocationModule.self) private var locationModule typealias UIViewControllerType = MapManagerView - + + @Environment(LocationModule.self) private var locationModule + func makeUIViewController(context: Context) -> MapManagerView { MapManagerView(locationModule: locationModule) } @@ -21,8 +22,6 @@ struct MapManagerViewWrapper: UIViewControllerRepresentable { } public class MapManagerView: UIViewController { - private var locationModule: LocationModule? - private enum Constants { static let geoSourceId = "GEOSOURCE" static let circleLayerId = "CIRCLELAYER" @@ -30,6 +29,8 @@ public class MapManagerView: UIViewController { static let countryLabelLayerId = "country-label" } + private var locationModule: LocationModule? + private lazy var mapView: MapView = { let map = MapView(frame: view.bounds) map.autoresizingMask = [.flexibleWidth, .flexibleHeight] diff --git a/StrokeCog/Map/StrokeCogMapView.swift b/StrokeCog/Map/StrokeCogMapView.swift index 9cff167..44e6bad 100644 --- a/StrokeCog/Map/StrokeCogMapView.swift +++ b/StrokeCog/Map/StrokeCogMapView.swift @@ -6,7 +6,7 @@ // Copyright © 2024 StrokeCog. All rights reserved. // -@_spi(Experimental) import MapboxMaps +import MapboxMaps import SwiftUI struct StrokeCogMapView: View { @@ -22,10 +22,6 @@ struct StrokeCogMapView: View { MapManagerViewWrapper() } } - - init() { - MapboxOptions.accessToken = "" - } } struct StrokeCogMapView_Previews: PreviewProvider { diff --git a/StrokeCog/Onboarding/StudyIDView.swift b/StrokeCog/Onboarding/StudyIDView.swift index fbdf415..450772a 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") { } } } diff --git a/StrokeCog/Resources/Localizable.xcstrings b/StrokeCog/Resources/Localizable.xcstrings index 9e1ab78..38fcb68 100644 --- a/StrokeCog/Resources/Localizable.xcstrings +++ b/StrokeCog/Resources/Localizable.xcstrings @@ -252,6 +252,16 @@ } } }, + "INVALID_STUDYID_MESSAGE" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You entered an invalid Study ID." + } + } + } + }, "LELAND_STANFORD_BIO" : { "localizations" : { "en" : { @@ -390,6 +400,16 @@ } } }, + "RETRY_BUTTON_LABEL" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Try Again" + } + } + } + }, "SCHEDULE_LIST_TITLE" : { "localizations" : { "en" : { @@ -411,11 +431,35 @@ } } }, - "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" + } + } + } }, "TASK_CONTEXT_ACTION_QUESTIONNAIRE" : { "localizations" : { @@ -466,12 +510,6 @@ } } } - }, - "Try Again" : { - - }, - "Welcome to the StrokeCog study. To get started, please enter your study ID." : { - }, "WELCOME_AREA1_DESCRIPTION" : { "localizations" : { @@ -563,9 +601,6 @@ } } } - }, - "You've entered an invalid study ID." : { - } }, "version" : "1.0" diff --git a/StrokeCog/Supporting Files/Info.plist b/StrokeCog/Supporting Files/Info.plist index 8c631ac..93baf58 100644 --- a/StrokeCog/Supporting Files/Info.plist +++ b/StrokeCog/Supporting Files/Info.plist @@ -6,6 +6,10 @@ ITSAppUsesNonExemptEncryption + MBXAccessToken + TOKEN + MGLMapboxMetricsEnabledSettingShownInApp + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes From b968fc2071dd3d1d2809fce951e7a4dfa55213cb Mon Sep 17 00:00:00 2001 From: Vishnu Ravi Date: Wed, 3 Apr 2024 12:49:35 -0400 Subject: [PATCH 12/15] Fix swiftlint warnings --- .swiftlint.yml | 2 -- StrokeCog/Map/MapboxView.swift | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) 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/Map/MapboxView.swift b/StrokeCog/Map/MapboxView.swift index a45747b..5855f50 100644 --- a/StrokeCog/Map/MapboxView.swift +++ b/StrokeCog/Map/MapboxView.swift @@ -48,6 +48,7 @@ public class MapManagerView: UIViewController { super.init(nibName: nil, bundle: nil) } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } From 246326cac4a0089edc0ce9e64c287b25ad2ff9ac Mon Sep 17 00:00:00 2001 From: Vishnu Ravi Date: Thu, 4 Apr 2024 19:15:03 -0400 Subject: [PATCH 13/15] Get both location permissions --- StrokeCog/Location/LocationModule.swift | 2 +- .../Onboarding/LocationPermissions.swift | 23 ++++++++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/StrokeCog/Location/LocationModule.swift b/StrokeCog/Location/LocationModule.swift index 652f383..f86150f 100644 --- a/StrokeCog/Location/LocationModule.swift +++ b/StrokeCog/Location/LocationModule.swift @@ -123,6 +123,6 @@ public class LocationModule: NSObject, CLLocationManagerDelegate, Module, Defaul } public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { - // TODO: Handle authorization change + authorizationStatus = manager.authorizationStatus } } diff --git a/StrokeCog/Onboarding/LocationPermissions.swift b/StrokeCog/Onboarding/LocationPermissions.swift index 049ba90..4705acd 100644 --- a/StrokeCog/Onboarding/LocationPermissions.swift +++ b/StrokeCog/Onboarding/LocationPermissions.swift @@ -5,6 +5,7 @@ // Created by Vishnu Ravi on 4/2/24. // +import OSLog import SpeziOnboarding import SpeziScheduler import SwiftUI @@ -16,6 +17,8 @@ struct LocationPermissions: View { @State private var locationProcessing = false + private let logger = Logger(subsystem: "StrokeCog", category: "Onboarding") + var body: some View { OnboardingView( @@ -26,7 +29,7 @@ struct LocationPermissions: View { subtitle: "LOCATION_PERMISSIONS_SUBTITLE" ) Spacer() - Image(systemName: "bell.square.fill") + Image(systemName: "mappin.and.ellipse") .font(.system(size: 150)) .foregroundColor(.accentColor) .accessibilityHidden(true) @@ -44,22 +47,30 @@ struct LocationPermissions: View { // 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 { - print("Could not request notification permissions.") + logger.debug("Could not request location permissions.") } - locationProcessing = false - - onboardingNavigationPath.nextStep() } ) } ) .navigationBarBackButtonHidden(locationProcessing) - // Small fix as otherwise "Login" or "Sign up" is still shown in the nav bar .navigationTitle(Text(verbatim: "")) + .onReceive(locationModule.$authorizationStatus) { status in + switch status { + case .authorizedWhenInUse: + locationModule.requestAuthorizationLocation() + case .authorizedAlways: + onboardingNavigationPath.nextStep() + locationProcessing = false + default: + break + } + } } } From 2b5f252aff761ea84711e44390ad992315ad1ead Mon Sep 17 00:00:00 2001 From: Vishnu Ravi Date: Thu, 4 Apr 2024 19:51:42 -0400 Subject: [PATCH 14/15] Save location data in Firebase --- StrokeCog.xcodeproj/project.pbxproj | 4 ++++ .../xcshareddata/xcschemes/StrokeCog.xcscheme | 4 ++-- StrokeCog/Location/LocationDataPoint.swift | 18 ++++++++++++++++ StrokeCog/Location/LocationModule.swift | 12 +++++++++-- StrokeCog/Onboarding/StudyIDView.swift | 2 +- StrokeCog/Resources/Localizable.xcstrings | 13 +++++++++--- StrokeCog/StrokeCogStandard.swift | 21 +++++++++++++++++++ 7 files changed, 66 insertions(+), 8 deletions(-) create mode 100644 StrokeCog/Location/LocationDataPoint.swift diff --git a/StrokeCog.xcodeproj/project.pbxproj b/StrokeCog.xcodeproj/project.pbxproj index 0c59537..a7866ec 100644 --- a/StrokeCog.xcodeproj/project.pbxproj +++ b/StrokeCog.xcodeproj/project.pbxproj @@ -69,6 +69,7 @@ 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 */; }; @@ -150,6 +151,7 @@ 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 = ""; }; @@ -337,6 +339,7 @@ children = ( 63F4C39A2BBCCCF80033D985 /* LocationModule.swift */, 63F4C39C2BBCCD200033D985 /* LocationUtils.swift */, + 63497B6F2BBF6ECE001F8419 /* LocationDataPoint.swift */, ); path = Location; sourceTree = ""; @@ -646,6 +649,7 @@ 2FE5DC3729EDD7CA004B9AB4 /* OnboardingFlow.swift in Sources */, 2F1AC9DF2B4E840E00C24973 /* StrokeCog.docc in Sources */, 2FF53D8D2A8729D600042B76 /* StrokeCogStandard.swift in Sources */, + 63497B702BBF6ECE001F8419 /* LocationDataPoint.swift in Sources */, A9720E432ABB68CC00872D23 /* AccountSetupHeader.swift in Sources */, 2FE5DC4029EDD7EE004B9AB4 /* FeatureFlags.swift in Sources */, 2FE5DC4629EDD7F2004B9AB4 /* Bundle+Image.swift in Sources */, diff --git a/StrokeCog.xcodeproj/xcshareddata/xcschemes/StrokeCog.xcscheme b/StrokeCog.xcodeproj/xcshareddata/xcschemes/StrokeCog.xcscheme index 4b87a49..fcbb02d 100644 --- a/StrokeCog.xcodeproj/xcshareddata/xcschemes/StrokeCog.xcscheme +++ b/StrokeCog.xcodeproj/xcshareddata/xcschemes/StrokeCog.xcscheme @@ -79,7 +79,7 @@ + isEnabled = "NO"> + isEnabled = "YES"> diff --git a/StrokeCog/Location/LocationDataPoint.swift b/StrokeCog/Location/LocationDataPoint.swift new file mode 100644 index 0000000..1591cdb --- /dev/null +++ b/StrokeCog/Location/LocationDataPoint.swift @@ -0,0 +1,18 @@ +// +// LocationDataPoint.swift +// StrokeCog +// +// Created by Vishnu Ravi on 4/4/24. +// + +import Foundation +import CoreLocation + +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 index f86150f..bb17f46 100644 --- a/StrokeCog/Location/LocationModule.swift +++ b/StrokeCog/Location/LocationModule.swift @@ -10,6 +10,8 @@ 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)? @@ -104,8 +106,14 @@ public class LocationModule: NSObject, CLLocationManagerDelegate, Module, Defaul onLocationsUpdated?(allLocations) previousLocation = point previousDate = Date() - - // TODO: Save to Firestore + + Task { + do { + try await standard?.add(location: point) + } catch { + print(error.localizedDescription) + } + } } } diff --git a/StrokeCog/Onboarding/StudyIDView.swift b/StrokeCog/Onboarding/StudyIDView.swift index 450772a..cd04d2f 100644 --- a/StrokeCog/Onboarding/StudyIDView.swift +++ b/StrokeCog/Onboarding/StudyIDView.swift @@ -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 38fcb68..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" : { @@ -461,6 +458,16 @@ } } }, + "STUDYID_VALIDATION_MESSAGE" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "A study ID should be at least 4 characters long." + } + } + } + }, "TASK_CONTEXT_ACTION_QUESTIONNAIRE" : { "localizations" : { "en" : { 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 From a27067dddf4680cc9dd80e226edff930ed23b1c0 Mon Sep 17 00:00:00 2001 From: Vishnu Ravi Date: Thu, 4 Apr 2024 19:54:48 -0400 Subject: [PATCH 15/15] Fix Swiftlint error --- StrokeCog/Location/LocationDataPoint.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StrokeCog/Location/LocationDataPoint.swift b/StrokeCog/Location/LocationDataPoint.swift index 1591cdb..d51faea 100644 --- a/StrokeCog/Location/LocationDataPoint.swift +++ b/StrokeCog/Location/LocationDataPoint.swift @@ -5,8 +5,8 @@ // Created by Vishnu Ravi on 4/4/24. // -import Foundation import CoreLocation +import Foundation struct LocationDataPoint: Codable { var currentDate: Date