WWDC 2024「位置情報へのアクセス許可の新機能」の内容試してみた

# はじめに

本田技研工業でiOSアプリ開発を担当している松野です。

WWDC2024「**位置情報へのアクセス許可の新機能**」で発表された新機能をいくつか試してみました。


:::note warn
実行環境は

- Xcode: 16.0 Beta 6
- Simulator: iPhone 15 Pro - iOS 18.0(22A5342a)

になります。

Beta版での検証のため皆様の手元で動作しない場合があります。
実際にこの記事内には過去のBeta版では動作しないコードが含まれています。
:::

# 去年の新機能の振り返りと今年の新機能概要

iOS17ではいくつかのstruct/classが追加されました。

主だったところではこれらが挙げられます。

- https://developer.apple.com/documentation/corelocation/cllocationupdate
- https://developer.apple.com/documentation/corelocation/clmonitor-2r51v
- https://developer.apple.com/documentation/corelocation/clbackgroundactivitysession-3mzv3

これによってConcurrency対応がされたり、Sessionを張ったりといったことが一部できるようになりました。

今年の新機能では、

- **CLBackgroundActivitySession**のフォアグラウンド版のような立ち位置である**CLServiceSession**の追加
- セッションを張ることによる権限の自動要求
- Diagnostic properties（診断プロパティ）の追加

が主なトピックになります。

# セッションと権限要求

従来では

1. LocationManagerを用意し、
2. 権限要求を行い、
3. LocationManagerの位置取得を開始し、
4. Delegateパターンで位置情報の更新を受け取り処理を行う

という工程が必要でした。

```swift
import CoreLocation

class LocationReflector: NSObject, CLLocationManagerDelegate {
    static let shared = LocationReflector()
    private let manager = LocationManager()
    
    override init() {
        super.init()
        manager.delegate = self
    }
    
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        if (manager.authorizationStatus == .notDetermined) {
            manager.requestWhenInUseAuthorization()
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        // Process locations[0]
    }
}
```

これが今後は下のように書くことができるようになります。[^1]

```swift
Task {
    let session = CLServiceSession(authorization: .whenInUse)
    
    for try await update in CLLocationUpdate.liveUpdates() {
        // Process update.location or update.authorizationDenied
    }
}
```

まずセッションについてです。

詳しいライフサイクルに関しては後述しますが、権限要求→位置情報取得開始→取得終了までの一連の処理を管理してくれる存在になります。

こちらのクラスには以下のinit関数が用意されており、ここで必要な権限や精度を指定します。

- [`init(authorization: CLServiceSession.AuthorizationRequirement)`](https://developer.apple.com/documentation/corelocation/clservicesession-pt7n/init(authorization:))
- [`init(authorization: CLServiceSession.AuthorizationRequirement, fullAccuracyPurposeKey: String)`](https://developer.apple.com/documentation/corelocation/clservicesession-pt7n/init(authorization:fullaccuracypurposekey:)) 
- 
![スクリーンショット 2024-07-16 15.40.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3079250/d3d44161-a403-efa2-0990-e0acb51a3811.png)
*https://developer.apple.com/jp/videos/play/wwdc2024/10212/ より引用*
![スクリーンショット 2024-07-16 15.41.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3079250/7d0072fa-2da8-f38e-9d33-a34e36c12701.png)
*https://developer.apple.com/jp/videos/play/wwdc2024/10212/ より引用*


このクラスのインスタンスが生成された際に位置権限が`NotDetermined`な状態だと、必要な権限要求を自動で行ってくれます。[^2]

また、デフォルトセッションが用意されているため、

```swift
Task {
    // let session = CLServiceSession(authorization: .whenInUse)
    
    for try await update in CLLocationUpdate.liveUpdates() {
        // Process update.location or update.authorizationDenied
    }
}
```

と記述するだけで、自動的に`CLServiceSession(authorization: .whenInUse)`のセッションが適用されます。

デフォルトセッションは無効にすることも可能で、その場合はInfo.plistに`CLRequireExplicitServiceSession` を指定すれば良いとのことです。[^3]

Ref：https://developer.apple.com/documentation/corelocation/suspending-authorization-requests

> Add the `CLRequireExplicitServiceSession` property to your app’s Info.plist file to opt into this control behavior.
> 

デフォルトセッションを無効にしたい場面としては以下のような例が挙げられていました。

- 一時的なFullアクセスを必要とするとき
- Always権限を必要とするとき
- 権限要求タイミングをハンドリングしたいとき

## セッションのライフタイムサイクルについて

![スクリーンショット 2024-07-16 15.53.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3079250/0654b8df-7771-afea-2f6d-e4d02b76b543.png)
*https://developer.apple.com/jp/videos/play/wwdc2024/10212/ より引用*

カメラ撮影時に位置情報を付与したり、
現在地を表示しながら地図を確認したり、
地図を確認しながら移動経路の検索をしたり

セッションは途切れたり、複数存在する場合があります。
CLServiceSessionではこれらをそのまま再現してくれます。
すなわち、セッションが必要なくなったらセッションは終了するし、複数セッションが許容されるということです。


![スクリーンショット 2024-07-16 15.57.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3079250/0308bde5-99c4-5e39-9af1-542e1b568b2d.png)
*https://developer.apple.com/jp/videos/play/wwdc2024/10212/ より引用*

また、ライフワークアプリで現在地を記録しつつ、ワーク中に音楽を聞くために別のアプリへ移動するといったケースを考えてみましょう。

この場合、基本的にはフォアグラウンドのセッションがあれば十分ですが、一時的にバックグラウンドへ移動する必要があります。

こういった場合でもバックグラウンドに移動したあとでも一定時間は位置情報の取得が行われます。

また、一定時間経過後はセッションが自動的に停止されます。

こちらは下の動画のように動作します。

<img width="200" alt="sim1.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3079250/3677f12e-3bb8-fb17-ce19-c94b47de44b4.gif">

---

![スクリーンショット 2024-07-17 8.20.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3079250/10357b89-15db-6760-1ebc-5d851a453bc0.png)
*https://developer.apple.com/jp/videos/play/wwdc2024/10212/ より引用*

また、CLServiceSessionはタスクキルをしても一定時間継続します。
こちらもバックグラウンド同様に一定時間経過後はセッションが切れます。

セッションが継続している間にアプリを立ち上げると、前のセッションが数秒だけ生き続けている状態を作り出すことができます。
そのためアプリ側でその間に新しいセッションを立ち上げれば位置情報を取得し続けるUXを達成することができるようになるそうです。

こちらも実際の様子は以下のようになります。
<img width="200" alt="sim2.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3079250/b70d0f6f-9f9b-84bc-df8f-9482326d6eb5.gif">


## Diagnostic properties（診断プロパティ）

### CLServiceSession

CLServiceSessionには権限等の状態を取得できるプロパティが存在しています。

こちらは以下のように実装することで取得できます。

```swift
let mySession = CLServiceSession(authorization: .whenInUse)

for try await diagnostic in  self.mySession.diagnostics {
    if diagnostic.authorizationDenied {
        // OK, let's let them pick a location instead?
    }
}
```

diagnosticは以下のフィールドを持つ構造体です。

- [`var alwaysAuthorizationDenied: Bool`](https://developer.apple.com/documentation/corelocation/clservicesession-pt7n/diagnostic/alwaysauthorizationdenied)
- [`var authorizationDenied: Bool`](https://developer.apple.com/documentation/corelocation/clservicesession-pt7n/diagnostic/authorizationdenied)
- [`var authorizationDeniedGlobally: Bool`](https://developer.apple.com/documentation/corelocation/clservicesession-pt7n/diagnostic/authorizationdeniedglobally)
- [`var authorizationRequestInProgress: Bool`](https://developer.apple.com/documentation/corelocation/clservicesession-pt7n/diagnostic/authorizationrequestinprogress)
- [`var authorizationRestricted: Bool`](https://developer.apple.com/documentation/corelocation/clservicesession-pt7n/diagnostic/authorizationrestricted)
- [`var fullAccuracyDenied: Bool`](https://developer.apple.com/documentation/corelocation/clservicesession-pt7n/diagnostic/fullaccuracydenied)
- [`var insufficientlyInUse: Bool`](https://developer.apple.com/documentation/corelocation/clservicesession-pt7n/diagnostic/insufficientlyinuse)
- [`var serviceSessionRequired: Bool`](https://developer.apple.com/documentation/corelocation/clservicesession-pt7n/diagnostic/servicesessionrequired)

ここで注意が必要なのが、権限周りのフィールドでは複数の値がtrueになるケースもあるということです。
（例：アプリ全体で位置情報拒否 → [`authorizationDenied`](https://developer.apple.com/documentation/corelocation/clservicesession-pt7n/diagnostic/authorizationdenied)と[`authorizationDeniedGlobally`](https://developer.apple.com/documentation/corelocation/clservicesession-pt7n/diagnostic/authorizationdeniedglobally)の両方がtrueになる）

### CLLocationUpdate / CLMonitor.Event

iOS17から使えていたこれらの構造体にも、診断プロパティが追加されました。

![スクリーンショット 2024-07-17 8.42.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3079250/83b25e02-58b0-ad69-8a83-6e5a86e06de4.png)
*https://developer.apple.com/jp/videos/play/wwdc2024/10212/ より引用*

CLLocationUpdateの.location以外、CLMonitor.Eventの.dateより下が今回追加された診断プロパティになります。
束ねてあるところはCLServiceSessionの診断プロパティとほぼ同等で、それに加えて各構造体特有のプロパティが存在している形になります。

### Tips

オンボーディング表示 → 権限要求ポップアップ → 権限選択後処理実行

というようなフローは以下のように実装することができるそうです。[^4]

[Suspending authorization requests | Apple Developer Documentation](https://developer.apple.com/documentation/corelocation/suspending-authorization-requests)

```swift
func doPromptingFlow() async {
    await showHelloPrompt()

    // Obtain a session. This causes Core Location to display the authorization prompt.
    let session = CLServiceSession.session(authorization: .whenInUse)

    // Wait for interaction with the prompot to complete (successfully or with denial).
    for try await diagnostic in session.diagnostics {
        if !diagnostic.authorizationRequestInProgress {
            // A denial occurred.
            break
        }
    }

    await doFurtherWork()
}
```

# おわりに

いくつかのコード実装をしつつiOS18での新機能を試してみました。

現在Beta中で動作が不安定なところがあり、いくつかのケースでは正常に動作しないことがありました。

今回のアップデートはこれまでの位置情報取得の手間を大幅に改善するものなので、今後のアップデートにも注目していきましょう。

[^1]: Xcode16.0 Beta 2, iOS 18.0 beta (22A5297f)時点では、明示的なセッションが位置情報権限要求中にCLLocationUpdate.liveUpdatesを呼び出すとエラーが生じ、アプリがクラッシュします。Xcode: 16.0 Beta 6, iOS 18.0(22A5342a)では正常に動作しています。

[^2]: Xcode: 16.0 Beta 6, iOS 18.0(22A5342a)時点では、権限に.alwaysを指定してもalwaysの権限要求をしてくれませんでした。

[^3]: Xcode16.0 Beta 2, iOS 18.0 beta (22A5297f)時点では動作しませんでした。Beta6での確認はまだしていません。

[^4]: Xcode16.0 Beta 2, iOS 18.0 beta (22A5297f)時点では、すでに権限選択済みの状態だとsession.diagnosticsが全く発火しないため動作しませんでした。Xcode: 16.0 Beta 6, iOS 18.0(22A5342a)では正常に動作しています。