
Live Activityを起動していれば位置情報の使用が「アプリの使用中は許可」でもバックグラウンドで位置情報が取得できるのか検証してみた
はじめに
こんにちは。加藤です。
先日、業務の中で Live Activityを起動していれば位置情報の使用が「アプリの使用中は許可」の場合でもバックグラウンドで継続して位置情報が取得できるのではないか?という情報を入手しました。
そこで本当にできるのか検証してみました。
先に結論
結論から言うと、以下の条件を満たしていれば
位置情報の使用が「アプリの使用中は許可」の場合でもバックグラウンドで継続して位置情報が取得できることがわかりました。
- Live Activityを起動していること
- 位置情報の取得に
CLLocationUpdate.liveUpdates
を使っていることCLLocationManager
ではダメでした
検証環境
- Xcode Version 16.4 (16F6)
- iPhone 15 Pro
- iOSバージョン 18.6.2
実装
Live Activty用のWidget Extensionを追加
Xcodeで新規プロジェクトを作成し、File > New > TargetでWidget Extensionを選択します。
この時、「Include Live Activity」にチェックを入れることでLive Activity用の雛形コードが生成されます。
Live Activity用のWidgetコードを編集
自動生成されたLive Activity用のWidgetコードを編集していきます。
とは言っても、Live ActivityのUI作成自体が本記事の目的ではないので、
必要最低限の変更として、ContentStateのパラメータ名をdynamicString
に変更しただけです。
ContentStateはLive Activityで動的に変更可能な部分です。
ここに端末で取得した緯度経度を表示してみましょう。
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)
}
}
}
Privacy – Location When In Use Usage Descriptionを設定
今回はアプリ使用中に位置情報を取得できるようにする必要があるので、
アプリのターゲットを選択し、InfoタブのCustom iOS Target Propertiesに
「Privacy – Location When In Use Usage Description」の項目を追加し、位置情報の利用目的を記載しておきましょう。
アプリ本体を実装
以下のようにアプリ本体を実装しました。
Live Activityの開始、更新、終了と位置情報の取得開始と終了ができるようにしました。
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?
///
/// ランダムな緯度経度を返します。
///
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") {
//locationManager.stopUpdatingLocation()
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 {
// アプリがフォアグラウンドに戻ったときにLive Activityの数が0ならactivityIdにnilを設定
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
}
// updateした時の通知用設定。titleとbodyはApple Watch専用。iPhoneとiPadはsoundのみ有効。
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
}
ポイントは位置情報の取得に CLLocationUpdate.liveUpdates
を使っている所です。
iOS 17.0から使えるみたいですが、位置情報の許諾を得るためのコードを書かなくても自動で許諾ダイアログを出してくれます。
CLLocationManager
を使っていた時代はまずは許諾を得て、ユーザーがOKしたら位置情報の取得を開始する流れだと記憶していますがだいぶ楽になったんですね(感慨深い...)
実行結果
実行結果を載せておきます。
アプリを起動し、Live Activityを起動してます。
その後に位置情報使用許可のダイアログで「アプリの使用中は許可」を選択すると位置情報の取得が開始されます。
アプリがフォアグラウンドにいる時はもちろん位置情報が取得され続けますが、
アプリがバックグラウンドに移行したりロック画面を表示してもLive Activityの緯度経度が更新されつづけているのでバックグラウンドの位置情報取得が継続できていることがわかると思います。
CLLocationManagerでの位置情報取得ではダメ
CLLocationManager
での位置情報を取得するパターンも試してみましたが、こちらはバックグラウンドに移行してから数秒すると位置情報の取得が止まってしまいました。
Live Activityを使ってさえいればバックグラウンドでの位置情報取得が可能というわけではなさそうです。
参考までにCLLocationManager
を使ったコードも載せてきます。
import ActivityKit
import CoreLocation
@Observable class LocationManager: NSObject {
let manager = CLLocationManager()
private(set) var location: CLLocation?
override init() {
super.init()
manager.delegate = self
}
func startUpdatingLocationIfPossible() {
let status = manager.authorizationStatus
if status == .authorizedAlways || status == .authorizedWhenInUse {
manager.startUpdatingLocation()
} else {
requestWhenInUseAuthorization()
}
}
func stopUpdatingLocation() {
print("位置情報の取得を停止しました。")
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)")
self.location = location
// 以下、Live Activityを更新
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")
}
}
}
ContentView.swift
の方はLocationManager
を使うようにしたことと、
位置情報取得開始・終了の処理のみ違うため差分だけ載せてきます。
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()
}
...
}
おわりに
今回はLive Activityを起動していれば位置情報の使用が「アプリの使用中は許可」の場合でもバックグラウンドで継続して位置情報が取得できるのかを検証しました。
Live AcitvityとCLLocationUpdate.liveUpdatesの組み合わせであれば可能であることがわかりました。
どなたかの参考になれば幸いです。
参考記事