Contents

Supporting live updates in SwiftUI and Mac Catalyst apps

Enable background events by adding lifecycle event support.

Overview

In iOS 17 and later, Core Location supports live updates using Swift concurrency’s async/await capability. In order to adopt live updates, SwiftUI and Mac Catalyst apps need to implement lifecycle event support that enables an app’s @main app to have explicit support for the creation and resumption of background run-loops. This enables the system to deliver Core Location events to the app and allows the delivery of events to resume in the event of return from background, launch of the app, or relaunch after a crash.

Adding lifecycle events to SwiftUI

To add support for life cycle events, you need to add three components to your app:

  1. A shared state using an ObservableObject that maintains instances of CLLocationManager and CLBackgroundActivitySession

  2. An AppDelegate object that provides the application(_:didFinishLaunchingWithOptions:) method that handles resuming background activities on return from background or an app relaunch

  3. An AppDelegate object in the SwiftUI or Mac Catalyst app’s @main structure

In your SwiftUI or Mac Catalyst App, add support for the AppDelegate by adding a shared state through an ObservableObject, and a UIApplicationDelegateAdaptor as an object the app’s @main structure maintains, as shown in the following example:

    import SwiftUI

    // Shared state that manages the `CLLocationManager` and `CLBackgroundActivitySession`.
    @MainActor class LocationsHandler: ObservableObject {
    
        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 isStationary = false
        @Published var count = 0
    
        @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")
            }
        }
    
    
        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()
                    for try await update in updates {
                        if !self.updatesStarted { break }  // End location updates by breaking out of the loop.
                        if let loc = update.location {
                            self.lastLocation = loc
                            self.isStationary = update.isStationary
                            self.count += 1
                            print("Location \(self.count): \(self.lastLocation)")
                        }
                    }
                } catch {
                    print("Could not start location updates")
                }
                return
            }
        }
    
        func stopLocationUpdates() {
            print("Stopping location updates")
            self.updatesStarted = false
            self.updatesStarted = false
        } 
    }

Next, create an instance of a UIKit AppDelegate class that conforms to SwiftUI’s ObservableObject protocol; this enables the AppDelegate to participate in the SwiftUI’s app-level shared state and manages the resumption of Core Location activities when needed.

    import Foundation
    import UIKit

    class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {   
    
        func application(_ application: UIApplication, didFinishLaunchingWithOptions
                         launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
            let locationsHandler = LocationsHandler.shared
        
            // If location updates were previously active, restart them after the background launch.
            if locationsHandler.updatesStarted {
                locationsHandler.startLocationUpdates()
            }
            // If a background activity session was previously active, reinstantiate it after the background launch.
            if locationsHandler.backgroundActivity {
                locationsHandler.backgroundActivity = true
            }
            return true
        }
    }

Finally, include the AppDelegate functionality in your app’s @main structure using a UIApplicationDelegateAdaptor:

    @main
    struct MyApp: App {
        @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
        var body: some Scene {
            WindowGroup {
                ContentView()
            }
        }
    }

See Also

Essentials