From 5e6f0f5dc32a62be8eeb89edf6d4e454ec95ebe3 Mon Sep 17 00:00:00 2001 From: Vishnu Ravi Date: Wed, 14 Aug 2024 00:02:07 -0400 Subject: [PATCH] Refactors location logic (#40) --- .github/workflows/build-and-test.yml | 2 +- LifeSpace.xcodeproj/project.pbxproj | 6 +- LifeSpace/Location/LocationModule.swift | 131 +++++++++--------- LifeSpace/Map/LifeSpaceMapView.swift | 5 +- LifeSpace/Onboarding/Consent.swift | 2 +- .../Onboarding/LocationPermissions.swift | 5 + LifeSpaceStandard.swift | 5 + 7 files changed, 84 insertions(+), 72 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index afa8885..052c42e 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -42,4 +42,4 @@ jobs: with: xcode-version: '15.4' - name: Build and Test - run: xcodebuild test -project LifeSpace.xcodeproj -scheme LifeSpace -destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ No newline at end of file + run: xcodebuild test -project LifeSpace.xcodeproj -scheme LifeSpace -destination 'platform=iOS Simulator,name=iPhone 15 Pro' diff --git a/LifeSpace.xcodeproj/project.pbxproj b/LifeSpace.xcodeproj/project.pbxproj index 7bab5a5..085b7da 100644 --- a/LifeSpace.xcodeproj/project.pbxproj +++ b/LifeSpace.xcodeproj/project.pbxproj @@ -792,7 +792,7 @@ CODE_SIGN_ENTITLEMENTS = "LifeSpace/Supporting Files/LifeSpace.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 14; + CURRENT_PROJECT_VERSION = 17; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; @@ -996,7 +996,7 @@ CODE_SIGN_ENTITLEMENTS = "LifeSpace/Supporting Files/LifeSpace.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 14; + CURRENT_PROJECT_VERSION = 17; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; @@ -1044,7 +1044,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 14; + CURRENT_PROJECT_VERSION = 17; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = ""; diff --git a/LifeSpace/Location/LocationModule.swift b/LifeSpace/Location/LocationModule.swift index ec22b91..2d4cda9 100644 --- a/LifeSpace/Location/LocationModule.swift +++ b/LifeSpace/Location/LocationModule.swift @@ -10,46 +10,29 @@ import Foundation import OSLog import Spezi + public class LocationModule: NSObject, CLLocationManagerDelegate, Module, DefaultInitializable, EnvironmentAccessible { @Dependency(LifeSpaceStandard.self) private var standard: LifeSpaceStandard? - - private(set) var manager = CLLocationManager() - public var allLocations = [CLLocationCoordinate2D]() - public var onLocationsUpdated: (([CLLocationCoordinate2D]) -> Void)? private let logger = Logger(subsystem: "LifeSpace", category: "Standard") - - private var previousLocation: CLLocationCoordinate2D? - private var previousDate: Date? - + private(set) var manager = CLLocationManager() + @Published var authorizationStatus = CLLocationManager().authorizationStatus @Published var canShowRequestMessage = true - - private var lastKnownLocation: CLLocationCoordinate2D? { - didSet { - guard let lastKnownLocation = lastKnownLocation else { - return - } - Task { - await self.appendNewLocationPoint(point: lastKnownLocation) - } - } - } + + public var allLocations = [CLLocationCoordinate2D]() + public var onLocationsUpdated: (([CLLocationCoordinate2D]) -> Void)? + private var lastSaved: (location: CLLocationCoordinate2D, date: Date)? override public required init() { super.init() manager.delegate = self - // If user doesn't have a tracking preference, default to true - if UserDefaults.standard.value(forKey: StorageKeys.trackingPreference) == nil { - UserDefaults.standard.set(true, forKey: StorageKeys.trackingPreference) - } - - // If tracking status is true, start tracking + /// If `trackingPreference` is set to true, we can start tracking. if UserDefaults.standard.bool(forKey: StorageKeys.trackingPreference) { self.startTracking() } - // Disable Mapbox telemetry + /// Disable Mapbox telemetry as required by the study protocol. UserDefaults.standard.set(false, forKey: "MGLMapboxMetricsEnabled") } @@ -69,10 +52,10 @@ public class LocationModule: NSObject, CLLocationManagerDelegate, Module, Defaul public func stopTracking() { self.manager.stopUpdatingLocation() self.manager.stopMonitoringSignificantLocationChanges() + self.manager.allowsBackgroundLocationUpdates = false logger.info("Stopping tracking...") } - public func requestAuthorizationLocation() { self.manager.requestWhenInUseAuthorization() self.manager.requestAlwaysAuthorization() @@ -88,55 +71,71 @@ public class LocationModule: NSObject, CLLocationManagerDelegate, Module, Defaul logger.error("Error fetching locations: \(error.localizedDescription)") } } - - /// 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) async { - // Check that we only append points if location tracking is turned on + + /// Adds a new coordinate to the map and database, + /// - Parameter coordinate: the new coordinate to add. + @MainActor + private func appendNewLocation(_ coordinate: CLLocationCoordinate2D) async { + let shouldAddLocation = await determineIfShouldAddLocation(coordinate) + + if shouldAddLocation { + updateLocalLocations(with: coordinate) + await saveLocation(coordinate) + } + } + + /// Determines if a location meets the criteria to be saved. + /// - Parameter coordinate: The `CLLocationCoordinate2D` of the location to be saved. + private func determineIfShouldAddLocation(_ coordinate: CLLocationCoordinate2D) async -> Bool { + /// Check if the user has set tracking `on` before adding the new location. guard UserDefaults.standard.bool(forKey: StorageKeys.trackingPreference) else { - return + return false } - 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 { - await fetchLocations() - add = true - } + /// Check if there is a previously saved point, so we can calculate the distance between that and the current point. + /// If there's no previously saved point, we can save the current point + guard let lastSaved else { + return true } - - if add { - // update local location data for map - allLocations.append(point) - onLocationsUpdated?(allLocations) - previousLocation = point - previousDate = Date() - - do { - try await standard?.add(location: point) - } catch { - logger.error("Error adding location: \(error.localizedDescription)") - } + + /// Check if the date of the current point is a different day then the last saved point. If so, + /// Refresh the locations array and save this point. + if Date().startOfDay != lastSaved.date.startOfDay { + await fetchLocations() + return true + } + + return LocationUtils.isAboveMinimumDistance( + previousLocation: lastSaved.location, + currentLocation: coordinate + ) + } + + /// Updates the local set of locations and the map with the latest location + /// - Parameter coordinate: The `CLLocationCoordinate2D` of the location to be saved. + private func updateLocalLocations(with coordinate: CLLocationCoordinate2D) { + allLocations.append(coordinate) + onLocationsUpdated?(allLocations) + lastSaved = (location: coordinate, date: Date()) + } + + /// Saves a location to Firestore via the Standard. + /// - Parameter coordinate: the `CLLocationCoordinate2D` of the location to be saved. + private func saveLocation(_ coordinate: CLLocationCoordinate2D) async { + do { + try await standard?.add(location: coordinate) + } catch { + logger.error("Error saving location: \(error.localizedDescription)") } } public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - // Check that we only append points if location tracking is turned on - guard UserDefaults.standard.bool(forKey: StorageKeys.trackingPreference) else { + guard let latestLocation = locations.first?.coordinate else { return } - - lastKnownLocation = locations.first?.coordinate + Task { + await appendNewLocation(latestLocation) + } } public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { diff --git a/LifeSpace/Map/LifeSpaceMapView.swift b/LifeSpace/Map/LifeSpaceMapView.swift index 24de243..5d316b3 100644 --- a/LifeSpace/Map/LifeSpaceMapView.swift +++ b/LifeSpace/Map/LifeSpaceMapView.swift @@ -64,10 +64,13 @@ struct LifeSpaceMapView: View { } } .onChange(of: scenePhase) { _, newPhase in - if newPhase == .active && trackingOn { + if newPhase == .active { refreshMap() } } + .onAppear { + refreshMap() + } } private var locationTrackingOverlay: some View { diff --git a/LifeSpace/Onboarding/Consent.swift b/LifeSpace/Onboarding/Consent.swift index abb8cfe..42a6b70 100644 --- a/LifeSpace/Onboarding/Consent.swift +++ b/LifeSpace/Onboarding/Consent.swift @@ -105,7 +105,7 @@ struct Consent: View { onboardingNavigationPath.nextStep() } .processingOverlay( - isProcessing: checkingConsentForms, + isProcessing: savingConsentForms, overlay: { VStack { Text("CONSENT_PROGRESS") diff --git a/LifeSpace/Onboarding/LocationPermissions.swift b/LifeSpace/Onboarding/LocationPermissions.swift index 7f59757..d9d3232 100644 --- a/LifeSpace/Onboarding/LocationPermissions.swift +++ b/LifeSpace/Onboarding/LocationPermissions.swift @@ -42,6 +42,11 @@ struct LocationPermissions: View { .onReceive(locationModule.$authorizationStatus) { status in switch status { case .authorizedAlways: + /// Set `trackingPreference` in UserDefaults + UserDefaults.standard.setValue(true, forKey: StorageKeys.trackingPreference) + /// Start tracking via location module + locationModule.startTracking() + /// Go to next step in onboarding onboardingNavigationPath.nextStep() case .authorizedWhenInUse: if isFirstRequest { diff --git a/LifeSpaceStandard.swift b/LifeSpaceStandard.swift index 787cb44..98913f7 100644 --- a/LifeSpaceStandard.swift +++ b/LifeSpaceStandard.swift @@ -125,6 +125,11 @@ actor LifeSpaceStandard: Standard, EnvironmentAccessible, HealthKitConstraint, O throw LifeSpaceStandardError.invalidStudyID } + // Check that we only save points if location tracking is turned on + guard UserDefaults.standard.bool(forKey: StorageKeys.trackingPreference) else { + return + } + let dataPoint = LocationDataPoint( currentDate: Date(), time: Date().timeIntervalSince1970,