
I tested whether location information can be obtained in the background with Live Activity running, even when location permission is set to "Allow While Using App"
Introduction
Hello. This is Kato.
Recently, I obtained information suggesting that if Live Activity is running, location information could be continuously acquired in the background even when location usage permission is set to "Allow While Using App".
So I decided to verify if this is actually possible.
Conclusion First
To conclude,
I found that with the combination of Live Activity and CLLocationUpdate.liveUpdates, location information can be acquired in the background without Background Modes settings.
Also, with the combination of Live Activity and CLLocationManager, location information can be acquired in the background if background location update settings are configured.
Testing Environment
- Xcode Version 16.4 (16F6)
- iPhone 15 Pro
- iOS version 18.6.2
Implementation
Adding Widget Extension for Live Activity
Create a new project in Xcode, then select File > New > Target and choose Widget Extension.
At this time, checking "Include Live Activity" will generate template code for Live Activity.
### Editing Widget Code for Live Activity
Let's edit the automatically generated Widget code for Live Activity.
That said, since creating the UI for Live Activity is not the main purpose of this article,
we'll make just the minimal necessary change, which is renaming the ContentState parameter to dynamicString
.
ContentState represents the part of Live Activity that can be dynamically changed.
Let's try displaying the latitude and longitude obtained from the device here.
import ActivityKit
import SwiftUI
import WidgetKit
struct LiveActivityAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
var dynamicString: String
}
// Fixed non-changing properties about your activity go here!
var name: String
}
struct LiveActivityLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: LiveActivityAttributes.self) { context in
// Lock screen/banner UI goes here
VStack {
Text("\(context.state.dynamicString)")
}
.activityBackgroundTint(Color.cyan)
.activitySystemActionForegroundColor(Color.black)
} dynamicIsland: { context in
DynamicIsland {
// Expanded UI goes here. Compose the expanded UI through
// various regions, like leading/trailing/center/bottom
DynamicIslandExpandedRegion(.leading) {
Text("Leading")
}
DynamicIslandExpandedRegion(.trailing) {
Text("Trailing")
}
DynamicIslandExpandedRegion(.bottom) {
Text("Bottom \(context.state.dynamicString)")
// more content
}
} compactLeading: {
Text("L")
} compactTrailing: {
Text("T \(context.state.dynamicString)")
} minimal: {
Text(context.state.dynamicString)
}
.widgetURL(URL(string: "http://www.apple.com"))
.keylineTint(Color.red)
}
}
}
```### Set "Privacy – Location When In Use Usage Description"
Since we need to enable location access while the app is in use,
select the app target, and in the Info tab under Custom iOS Target Properties,
add the item "Privacy – Location When In Use Usage Description" and describe the purpose for using location information.
### Implementing the main app
I implemented the main app as follows.
It allows starting, updating, and ending Live Activities, as well as starting and stopping location tracking.
```swift: ContentView.swift
import ActivityKit
import CoreLocation
import SwiftUI
struct ContentView: View {
@Environment(\.scenePhase) var scenePhase
@State private var activityId: String? = Activity<LiveActivityAttributes>.activities.first?.id
@State private var updates: CLLocationUpdate.Updates?
@State private var location: CLLocation?
///
/// Returns random latitude and longitude.
///
private func getRamdomLocation() -> CLLocation {
return CLLocation(
latitude: Double.random(in: 35.670256535824050...35.670256535824060),
longitude: Double.random(in: 139.75288455339030...139.75288455339040)
)
}
var body: some View {
VStack(spacing: 16) {
Text("Live Activity").fontWeight(.bold).font(.title)
Text("ActivityId: " + "\(activityId ?? "Not Started")")
Button("Start Live Activity") {
guard activityId == nil else {
print("Cannot create 2 or more Live Activities.")
return
}
activityId = startLiveActivity()
}
Button("Update Live Activity") {
guard let activityId = activityId else { return }
Task {
await updateLiveActivity(targetActivityId: activityId, dynamicString: "🤩")
}
}
Button("End Live Activity") {
guard let activityId = activityId else { return }
Task {
await endLiveActivity(targetActivityId: activityId)
self.activityId = nil
}
}.padding(.bottom, 48)
Text("Location").fontWeight(.bold).font(.title)
VStack {
Text("Latitude: \(getRamdomLocation().coordinate.latitude)")
Text("Longitude: \(getRamdomLocation().coordinate.longitude)")
}
Button("Start Updating Location") {
updates = CLLocationUpdate.liveUpdates()
guard let updates else { return }
Task {
for try await update in updates {
if let location = update.location {
self.location = location
}
if update.location != nil, let activityId = activityId {
await updateLiveActivity(
targetActivityId: activityId,
dynamicString:
"\(getRamdomLocation().coordinate.latitude), \(getRamdomLocation().coordinate.longitude)"
)
}
}
}
}
Button("Stop Updating Location") {
guard let updates = updates else { return }
Task {
for try await _ in updates {
break
}
}
}
.onChange(
of: scenePhase,
{ oldValue, newValue in
if newValue == .active && Activity<LiveActivityAttributes>.activities.count == 0 {
// Set activityId to nil if the number of Live Activities is 0 when the app returns to the foreground
activityId = nil
}
}
)
}
}
}
private func startLiveActivity() -> String? {
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
print("areActivitiesEnabled is false.")
return nil
}
do {
let attributes = LiveActivityAttributes(name: "")
let initialState = LiveActivityAttributes.ContentState(dynamicString: "😀")
let activity = try Activity.request(
attributes: attributes,
content: .init(state: initialState, staleDate: nil)
)
print("Start live activity successfully. id: \(activity.id)")
return activity.id
} catch {
print(error)
return nil
}
}
private func updateLiveActivity(targetActivityId: String, dynamicString: String) async {
guard let activity = findActivity(targetActivityId: targetActivityId) else {
return
}
// Notification settings for updates. Title and body are for Apple Watch only. Only sound is effective for iPhone and iPad.
let alertConfig = AlertConfiguration(title: "", body: "", sound: .default)
await activity.update(
.init(state: LiveActivityAttributes.ContentState(dynamicString: dynamicString), staleDate: nil),
alertConfiguration: alertConfig
)
print("Update activity successfully. id: \(activity.id)")
}
private func endLiveActivity(targetActivityId: String) async {
guard let activity = findActivity(targetActivityId: targetActivityId) else {
return
}
let activityContent = activity.content
await activity.end(activityContent, dismissalPolicy: .immediate)
print("End activity successfully. id: \(activity.id)")
}
private func findActivity(targetActivityId: String) -> Activity<LiveActivityAttributes>? {
guard
let activity = Activity<LiveActivityAttributes>.activities.first(where: {
$0.id == targetActivityId
})
else {
print("Not found activity.")
return nil
}
return activity
}
```The key point is using [`CLLocationUpdate.liveUpdates`](https://developer.apple.com/documentation/corelocation/cllocationupdate/liveupdates(_:)) for location acquisition.
It seems to be available from iOS 17.0, and it automatically displays the permission dialog without having to write code to obtain location permission.
In the era of using `CLLocationManager`, I remember we first had to get permission, and only after the user approved could we start acquiring location information, but it's become much easier now (feeling nostalgic...)
## Execution Result
Here's the execution result.
https://youtube.com/shorts/t6mQkNuYuRI?feature=share
I launched the app and started the Live Activity.
After selecting "Allow While Using App" in the location permission dialog, location acquisition begins.
Of course, location information continues to be acquired while the app is in the foreground, but
even when the app moves to the background or the lock screen is displayed, the latitude and longitude of the Live Activity keep updating, showing that background location acquisition is continuing successfully.## Configuration needed to get location information in the background using CLLocationManager
When using `CLLocationManager` to get location information in the background, you need to turn ON Location updates in Background Modes and set the `CLLocationManager` property `allowsBackgroundLocationUpdates` to `true`.

Without this setting, location information acquisition stopped after a few seconds when the app went into the background.
What we can understand from this is that using Live Activity alone does not enable location information acquisition in the background.
Here's the code using `CLLocationManager`.
```swift: LocationManager.swift
import ActivityKit
import CoreLocation
@Observable class LocationManager: NSObject {
let manager = CLLocationManager()
private(set) var location: CLLocation?
override init() {
super.init()
manager.delegate = self
// Set to true after turning ON Location updates in Background Modes
manager.allowsBackgroundLocationUpdates = true
// Property that determines whether the OS automatically stops location updates when it detects no change in location.
// If this is not set to false, users will need to relaunch the app to resume location updates when they are paused in the background.
manager.pausesLocationUpdatesAutomatically = false
}
func startUpdatingLocationIfPossible() {
let status = manager.authorizationStatus
if status == .authorizedAlways || status == .authorizedWhenInUse {
manager.startUpdatingLocation()
} else {
requestWhenInUseAuthorization()
}
}
func stopUpdatingLocation() {
print("Location tracking stopped.")
manager.stopUpdatingLocation()
}
private func requestWhenInUseAuthorization() {
manager.requestWhenInUseAuthorization()
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
let status = manager.authorizationStatus
switch status {
case .authorizedAlways:
print("authorizedAlways")
case .authorizedWhenInUse:
print("authorizedWhenInUse")
case .denied:
print("denied")
case .notDetermined:
print("notDetermined")
case .restricted:
print("restricted")
@unknown default:
break
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
print("Location acquisition successful: \(location)")
self.location = location
// Update Live Activity below
guard let activity = Activity<LiveActivityAttributes>.activities.first else {
return
}
Task {
await activity.update(
.init(
state: LiveActivityAttributes.ContentState(
dynamicString: "\(location.coordinate.latitude), \(location.coordinate.longitude)"
),
staleDate: nil
),
alertConfiguration: AlertConfiguration(title: "", body: "", sound: .default)
)
print("Update activity based on location update")
}
}
}
```For the `ContentView.swift` file, I've made changes to use `LocationManager` and the only differences are in the location updating start/stop processes, so I'm just showing the differences.
```swift: ContentView.swift
struct ContentView: View {
...
@State private var locationManager = LocationManager()
...
VStack {
Text("Latitude: \(locationManager.location?.coordinate.latitude ?? 0)")
Text("Longitude: \(locationManager.location?.coordinate.longitude ?? 0)")
}
Button("Start Updating Location") {
locationManager.startUpdatingLocationIfPossible()
}
Button("Stop Updating Location") {
locationManager.stopUpdatingLocation()
}
...
}
Conclusion
In this article, we verified whether location information could be continuously acquired in the background with the "Allow While Using App" permission if a Live Activity is running.
We found that with the combination of Live Activity and CLLocationUpdate.liveUpdates, location information can be acquired in the background without Background Modes settings.
Additionally, when using CLLocationManager
, background location acquisition is possible with the appropriate settings.
I hope this information is helpful to someone.
Reference Articles