Background location tracking challenge

I have a driving tracking app I'm working on that properly starts tracking and logging location when the app is in the foreground or background. I use a buffer/queue to keep recent locations so when a trip ramps up to driving speed I can record that to work back to the start location just before the trip starts. This works great, however, in background mode when the user does not have the app open it will record locations but not until a significant location change is detected. The buffering I do is lost and the location only starts tracking several hundred yards or more after the trip has started. Does anyone have any suggestions or strategies to handle this chicken and the egg scenario?

Accepted Reply

The new CoreLocation APIs introduced in iOS 17 will solve that problem, as they will relaunch your app without relying on significant location change.

Discover streamlined location updates: https://developer.apple.com/videos/play/wwdc2023/10180/

Meet Core Location Monitor: https://developer.apple.com/videos/play/wwdc2023/10147/

Add a Comment

Replies

The new CoreLocation APIs introduced in iOS 17 will solve that problem, as they will relaunch your app without relying on significant location change.

Discover streamlined location updates: https://developer.apple.com/videos/play/wwdc2023/10180/

Meet Core Location Monitor: https://developer.apple.com/videos/play/wwdc2023/10147/

Add a Comment

Per usual the Apple documentation is sparse at best and while I appreciate the demo example there are some gaps there too. So the following issues/questions:

  • The video and liveUpdatesSample app talk about activating the background activity session which supposedly allows your code to operate in the app is terminated by the user. However, no practical example is provided to show the path to do this successfully. I presume that the code in the startedUpdates would continue to execute if started and the background session is turned on but that is not what happens. Nothing continues or resumes when you relaunch the app.
  • The liveUpdatesSample app in the simulator always thinks that the device is not stationary even though I just have Apple set for location
  • in the liveUpdateSamples app it's also not clear if/when the background activity flag should be set. I'm guessing it's just a value to be picked up by the AppDelegate when that runs but it does not seem to control any behavior other than to be just a saved boolean.

I would be very helpful to have an example that makes it clearer when and how to ensure and validate that processing is happening in the background when the app is running or terminated.

I would appreciate any insights or guidance on how to ensure my code is running in the background.

Here is where I start the updates:

@MainActor class LocationsHandler: ObservableObject {
    let logger = Logger(subsystem: "com.***.yyy", category: "LocationsHandler")
    
    static let shared = LocationsHandler()  // Create a single, shared instance of the object.

    private let manager: CLLocationManager
    private var background: CLBackgroundActivitySession?

    @Published var lastLocation = CLLocation()
    @Published var count = 0
    @Published var isStationary = false

    @Published
    var updatesStarted: Bool = UserDefaults.standard.bool(forKey: "liveUpdatesStarted") {
        didSet { UserDefaults.standard.set(updatesStarted, forKey: "liveUpdatesStarted") }
    }

    @Published
    var backgroundActivity: Bool = UserDefaults.standard.bool(forKey: "BGActivitySessionStarted") {
        didSet {
            backgroundActivity ? self.background = CLBackgroundActivitySession() : self.background?.invalidate()
            UserDefaults.standard.set(backgroundActivity, forKey: "BGActivitySessionStarted")
            print("** background actvity changed to: \(backgroundActivity)")
        }
    }
    
    private init() {
        self.manager = CLLocationManager()  // Creating a location manager instance is safe to call here in `MainActor`.

    }

    func startLocationUpdates() {
        if self.manager.authorizationStatus == .notDetermined {
            self.manager.requestWhenInUseAuthorization()
        }
        self.logger.info("** Starting location updates")
        Task() {
            do {
                self.updatesStarted = true
                
                let updates = CLLocationUpdate.liveUpdates()
                
                var locationUpdateCounter = 0
                for try await update in updates {
                    
                    /// If updates are not started the break out of the loop
                    if !self.updatesStarted { break }
                    
                    /// If the location is not nill then we can process the update
                    if let loc = update.location {
                        
                        /// Update the Published variables
                        self.lastLocation = loc
                        self.isStationary = update.isStationary
                        self.count += 1
                        
                        /// If we are not stationary then then process the lcation
                        if update.isStationary == false {
                            locationUpdateCounter += 1
                            if locationUpdateCounter >= UserSettings.init().trackingSampleRate {
                                saveLocationUpdates(location: self.lastLocation)
                                locationUpdateCounter = 0
                            }
                        }
                    }
                }
            } catch {
                self.logger.error("** Could not start location updates")
            }
            return
        }
    }
    func stopLocationUpdates() {
        self.logger.info("** Stopping location updates")
        self.updatesStarted = false
    }
    
    func saveLocationUpdates(location: CLLocation) {
        do {
            // Access the sharedModelContainer
            guard let container = AppEnvironment.sharedModelContainer else {
                LogEvent.print(module: "LocationsHandler.saveLocation()", message: "shared model container has not been initialized")
                return
            }
            
            let context = ModelContext(container)
            
            let entry = GpsJournalSD(
                timestamp: Date(),
                longitude: location.coordinate.longitude,
                latitude: location.coordinate.latitude,
                speed: location.speed,
                processed: false,
                code: "",
                note: ""
            )
            
            context.insert(entry)
            
            print("**+ Location saved: \(entry.timestamp) \(formatMPH(convertMPStoMPH( entry.speed))) mph")
        }
    }
}

My saveLocationUpdate just writes data to swiftdata and that works fine with the app is in the foreground/background (but doesn't update if app terminated)

Here is the code in the appDelegate:

class AppDelegate: NSObject, UIApplicationDelegate {
    
    static let shared = AppDelegate()
    
    class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
        
        let logger = Logger(subsystem: "com.***.yyy", category: "AppDelegate")
        
        func application(_ application: UIApplication, didFinishLaunchingWithOptions
                         launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {

            /// Set as a singleton.  Important otherwise you get competing location handlers
            let locationsHandler = LocationsHandler.shared
            
            // If location updates were previously active, restart them after the background launch.
            if locationsHandler.updatesStarted {
                self.logger.info("** Restart liveUpdates Session")
                locationsHandler.startLocationUpdates()
            }
            // If a background activity session was previously active, reinstantiate it after the background launch.
            if locationsHandler.backgroundActivity {
                self.logger.info("** Reinstantiate background activity session")
                locationsHandler.backgroundActivity = true
            }
            return true
        }
    }
}

While it did mention in the text of the video that you have to have the background activity session enabled before you start the updates it was bit out of context in terms of a working example per the sample code. Enabling the background activity this way did the trick.

                self.updatesStarted = true
                
                // TODO: Looks like you have to add this here.
                backgroundActivity = true
                
                let updates = CLLocationUpdate.liveUpdates()