In this article, I would like to cover live location updates while an iOS application is in the background. How they are different from usual in-app updates, what changes need to be made to the project, and some techniques to make our lives easier.
Project Setup
Let’s start with the setup steps that are required for an iOS project to be able to get location updates in the background.
Project Info (Plist)
Project > Info
We need to add the privacy explanation strings:
- Privacy – Location When In Use Usage Description – required for location updates while the application is in the foreground.
- Privacy – Location Always and When In Use Usage Description – required for background location updates.
Note: Two are required because the process of acquiring consent for ‘Always’ mode is a two-step process; this will be covered further in the article.
Project Capabilities – Background Modes
Project > Signing & Capabilities > Background Modes
We need to enable the background mode for location updates.
Authorization
To receive background location updates, we should get ‘Always’ location authorization status.
This process is done in two steps:
- We call
CLLocationManager.requestAlwaysAuthorization(), which shows a dialog that prompts the user to deny, allow once, or allow while the app is in use. - At some point decided by iOS, it will show a second prompt that will allow the user to change the authorization status to ‘Always’.
In my experience, this “some point” from step two occurs when the application returns from the background and location updates (with step one approval) were disabled by the system. Or, if the application has used background updates for long enough, it will show the step two prompt when the application enters background mode.
There is a possibility to receive ‘Always’ authorization status in one go if the current authorization status is notDetermined. We can ask for requestWhenInUseAuthorization, wait for the result, and if the result is ‘When In Use’, immediately ask for requestAlwaysAuthorization. It is possible, but it is an obviously bad UX experience; showing two authorization prompts in a row, asking for more and more access, is just rude.
I wrote a simple manager to simplify the authorization routine and provide authorization status updates as a Combine Publisher:
@MainActor
public protocol ILocationAuthorizationManager: Observable {
var authorizationStatus: CLAuthorizationStatus { get }
var authorizationStatusPublisher: AnyPublisher<CLAuthorizationStatus, Never> { get }
func requestAlwaysAuthorization()
func requestWhenInUseAuthorization()
}
@Observable
public class LocationAuthorizationManager: NSObject, ILocationAuthorizationManager, CLLocationManagerDelegate {
private let logTag = "LocationAuthorizationManager"
private let authorizationStatusPublisherPassthrough: PassthroughSubject<CLAuthorizationStatus, Never> = .init()
private let locationManager: CLLocationManager
public private(set) var authorizationStatus: CLAuthorizationStatus {
didSet {
authorizationStatusPublisherPassthrough.send(authorizationStatus)
}
}
public var authorizationStatusPublisher: AnyPublisher<CLAuthorizationStatus, Never> {
authorizationStatusPublisherPassthrough.eraseToAnyPublisher()
}
public func requestWhenInUseAuthorization() {
locationManager.requestWhenInUseAuthorization()
}
public func requestAlwaysAuthorization() {
locationManager.requestAlwaysAuthorization()
}
public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
self.authorizationStatus = locationManager.authorizationStatus
}
override init() {
let locationManager = CLLocationManager()
self.locationManager = locationManager
self.authorizationStatus = locationManager.authorizationStatus
super.init()
locationManager.delegate = self
}
}
Receiving Location Updates In Background
Receiving location updates in the background works the same way as normal, with two important changes:
- We should indicate to the system that we want to keep receiving background updates by starting a
CLBackgroundActivitySessionand later invalidating it when updates are no longer required. - We should set the
LocationManagerpropertyallowsBackgroundLocationUpdatestotrueandpausesLocationUpdatesAutomaticallytofalse.
Here is my code responsible for this:
public enum BackgroundLocationDataProviderError: Error {
case notAuthorized
case locationManagerError(error: Error)
}
public protocol IBackgroundLocationDataProvider {
var backgroundLocationPublisher: AnyPublisher<CLLocation, BackgroundLocationDataProviderError> {get}
}
public class BackgroundLocationDataProvider: DynamicPublisher<CLLocation, BackgroundLocationDataProviderError>, CLLocationManagerDelegate, IBackgroundLocationDataProvider {
private let locationManager: CLLocationManager = .init()
private var backgroundActivitySession: CLBackgroundActivitySession?
public var backgroundLocationPublisher: AnyPublisher<CLLocation, BackgroundLocationDataProviderError> {
eraseToAnyPublisher()
}
public override init() {
super.init()
setup()
}
private func setup() {
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = false
locationManager.delegate = self
}
override public func start() {
guard [.authorizedAlways, .authorizedWhenInUse].contains(locationManager.authorizationStatus) else {
fail(with: .notAuthorized)
return
}
locationManager.startUpdatingLocation()
backgroundActivitySession = .init()
}
override public func stop() {
locationManager.stopUpdatingLocation()
backgroundActivitySession?.invalidate()
}
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
send(location)
}
}
public func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) {
fail(with: .locationManagerError(error: error))
}
}
Note: This DataProvider is based on my Lazy/Dynamic Combine Publisher code; more details here: https://www.linkedin.com/pulse/building-dynamic-data-sources-swift-combine-lazy-andrei-maksimovich-zq0uf/
How to handle background mode
We need to be able to know when location updates are happening in the background so we can adjust the code to reduce calculations and remove UI updates. Here are a helper SwiftUI modifier and View extension that can assist us with this task:
public struct ActionOnNotificationCenterEventViewModifier: ViewModifier {
private let action: () -> Void
private let notificationName: Notification.Name
public init(notificationName: Notification.Name, action: @escaping () -> Void) {
self.action = action
self.notificationName = notificationName
}
public func body(content: Content) -> some View {
content
.onReceive(NotificationCenter.default.publisher(for: notificationName)) {_ in
action()
}
}
}
public extension View {
func onWillEnterForeground(action: @escaping () -> Void) -> some View {
modifier(ActionOnNotificationCenterEventViewModifier(notificationName: UIScene.willEnterForegroundNotification, action: action))
}
func onDidEnterBackground(action: @escaping () -> Void) -> some View {
modifier(ActionOnNotificationCenterEventViewModifier(notificationName: UIScene.didEnterBackgroundNotification, action: action))
}
func onWillTerminate(action: @escaping () -> Void) -> some View {
modifier(ActionOnNotificationCenterEventViewModifier(notificationName: UIApplication.willTerminateNotification, action: action))
}
}
Note: onWillTerminate does not work when the user “swipes away” an application (no events are fired in this situation); going to the background should be treated as the last chance to save important data.
Using these simple view modifiers, we can change model behavior. In my test/example project, I’m switching the model from live UI and model updates to an accumulation of location updates, handling them when the application returns to the foreground. This is, of course, an oversimplification; in a real application, we should also discard stationary updates, updates that are too close, process accumulated updates as detached tasks, etc.
Conclusion
While background location updates initialized by the UI are relatively straightforward, they do come with a few technical caveats that require careful attention. Ultimately, our responsibility as developers is to manage these resources efficiently by avoiding background UI updates, minimizing unnecessary calculations, and being mindful of battery consumption to ensure a robust and performant experience for the user.
Full source code of the example project can be found here: https://github.com/AndreiMaksimovich/SwiftDeveloperBlog/tree/main/05%20-%20Background%20Location