BackgroundTasks（AppRefreshTasks & ProcessingTasks）

# この記事は

iOS13で利用可能なBackgroundTasksフレームワークを利用した、アプリのバックグラウンド処理についてのまとめと挙動の検証記事です。

# 機能概要

- 特徴
    - 以下のようなメンテナンスタイプのタスクに対して最適な仕組み
        - サーバーとの同期
        - データベースのクリーンアップ
        - クラウドへのバックアップ
    - アプリがフォアグランドでないタイミングで任意の処理を実行させることができる
        - これによりユーザー操作の邪魔をせずに必要なタスクを後回しにして実行させられる
    - 以下の2種類のモードがある
        1. Background App Refresh Tasks （Background Fetch）
        2. Background Processing Tasks

### Background App Refresh Tasks (Background Fetch)

- 特徴
    - iOS7で追加された `BackgroundFetch` がiOS13でアップデートされたもの
    - ユーザーの普段の利用傾向に合わせて、定期的にアプリの状態を最新化させることができる仕組み
- 実行タイミング
    - ユーザーの過去の行動からアプリケーションの起動頻度や時間帯が決定される
    - ユーザーが普段よくアプリを使うタイミングの少し前に起動される
    - 頻繁に使わないアプリケーションの場合には起動も少なくなる
    - 最早開始日時の指定は可能だが、1週間以内に設定することが推奨されている
- 起動可能状態
    - ユーザーにより強制的にkillされていない状態
        - システムによってkillされた場合は、起動し直してくれる
- 実行可能時間
    - 30秒間
- 備考
    - iOS12以前で利用していた以下のAPIはdeprecatedになった
        - `UIApplication.setMinimumBackgroundFetchInterval(_:)`
        - `UIApplicationDelegate.application(_:performFetchWithCompletionHandler:)`
- 所感
    - 「ユーザーがアプリを起動した際には、すでに最新のデータが取れている」といった体験を作ることができる
    - ただし、あくまでシステムがよしなに実行してくれるだけで必ずしも実行されるとは限らないので、必須処理をこれで担保するのは危険

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41486/d00a5f7d-42a4-4c09-987f-6f3b244669b2.jpeg"/>


### Background Processing Tasks

- 特徴
    - iOS13から利用可能な新機能
    - 以下のような細かい実行条件を設定できる （`App Refresh Tasks` はできない）
        - Wi-Fi接続あり
        - 充電中
    - CPU Monitor （バックグラウンドでCPUを使いすぎているAppを自動でkillする機能） をOFFにすることができる
        - （「充電中のみ実行可能」にすることで実現可能）
- 実行タイミング
    - 指定した実行可能条件に応じて、システムが判断して実行する
    - フォアグラウンドでリクエストされた場合や、アプリが最近使用されている場合にタスクが実行される
- 起動可能状態
    - デバイスがアイドル状態のとき
        - ユーザーがデバイスの使用を開始してしまうと、システムは実行中のバックグラウンド処理タスクを終了させる
        - （AppRefreshTasksは影響を受けない）
    - ユーザーにより強制的にkillされていない状態
        - システムによってkillされた場合は、起動し直してくれる
- 実行可能時間
    - 数分間に及ぶ実行も可能
- 所感
    - AppRefreshTasksに担わせることができない重たい処理を行わせるのに良い
    - しかしAppRefreshTasksと同様に確実に実行される保証はないので、実行されることを前提とした実装は避けた方がよさそう

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41486/9d2c3284-f4ce-7ab2-12b3-bf77adb27ac2.jpeg"/>


# 準備

1. Capabilityの設定

Background App Refresh Tasksの場合は `Background fetch` をチェックし。
Background Processing Tasksの場合は `Background processing` をチェックする、

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41486/e37f1702-30cd-4d54-624a-9c8075a65cc4.png"/>


2\. Info.plistの設定

`Permitted background task scheduler identifiers` にタスク毎のIDを設定する。
リバースDNSで他のフレームワークとの衝突を避け、ユニークになるように設定する。
（タスクIDは後述する `BGTaskScheduler` にタスクを登録・スケジューリングする際に利用する。）


<img width="700" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41486/e00aaebb-532c-16f3-56db-106aa2f2cc21.png"/>

# 実装

### サンプルコード

https://github.com/chocoyama/BackgroundSamples/blob/master/BackgroundSample/Views/BackgroundTasksView.swift

※ 詳細な解説はサンプルコード内にコメント文でも記述しているが、重要な部分のみ抜粋する

### 1. タスクの登録

タスクの登録は、BackgroundTasksフレームワークが提供する `BGTaskScheduler` に対して行う。

- **BGTaskScheduler**
    - タスクのタイプに応じたTaskRequestオブジェクトを作成することで、アプリ動作中にタスクを登録できる
        - `Background App Refresh Tasks` => `BGAppRefreshTaskRequest`
        - `Background Processing Tasks` => `BGProcessingTaskRequest`
    - 常に「バッテリー残量」「アプリケーション使用時間」「通信状態」のようなシステム状態を監視する
    - 必要なシステム状態とポリシーが満たされればタスクが実行される
        - タスク実行時は、バックグラウンドでアプリが起動され、対応するBGTaskオブジェクトが届けられる
        - タスクの完了は `setTaskCompleted` を呼び出すことで行い、これにより起動されたアプリを停止する
    - タスク起動は複数同時に行えるが、実行可能時間の割り当てはタスクごとではなく起動ごとに割り当てられるので注意

taskIdentifierにはInfo.plistに設定したタスクのIDを渡す。
launchHandlerに受け渡すクロージャは、システムによってタスクが起動される際に呼び出される。
そのため、ここにはバックグラウンドで行いたい処理自体を記述する。
（ハンドラを呼び出すキューを明示的に指定したい場合は、第二引数に受け渡す）

```swift
import BackgroundTasks

BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier, using: nil, launchHandler: { task in
    // タスクが実行された時の処理を記述する
}
```

### 2. 完了のハンドリング

目的とするタスクが完了したら、速やかに `setTaskCompleted` を呼び出す。
これを適切に呼び出さないと、次回の起動パフォーマンスに影響を及ぼしてしまう。
また、expirationHandlerが呼ばれた際もこれは呼び出す必要がある。
※ UISceneのアプリは `UIApplication.requestSceneSessionRefresh` も実行する。

```swift
task.setTaskCompleted(success: true)
```

### 3. 失効のハンドリング

launchHandlerで受け渡されてくる `BGTask` オブジェクトには `expirationHandler` プロパティが存在する。
バックグラウンド処理は、タイムアウトやシステム状態の悪化などにより、タスクを完了させられずに終了する可能性が比較的高い。
そのため、このハンドラを設定してタスクが失効したタイミングの処理を予め設定しておく。

```swift
task.expirationHandler = {
    // キャンセル処理を実行
}
```

### 4. タスクのスケジューリング

`submit` メソッドでタスクをスケジューリングする。
この時、`register` の時と同様にInfo.plistに設定したタスクIDを指定する。

既に同一IDのタスクが未実行状態でキューに溜まっていた場合、以前のタスクリクエストは置き換えられる。
また、最大で「AppRefreshTask x 1」と「ProcessingTask x 10」までしかスケジュールできないので、最大数を超えてスケジューリングしようとするとエラーが発生する。


**BGAppRefreshTaskRequestの場合**

```swift
func schedule() {
    let request = BGAppRefreshTaskRequest(identifier: taskIdentifier)
    
    // 実行開始時期を遅らせる
    // iOS12以前の、setMinimumBackgroundFetchIntervalと同じ動きをする
    // また、設定値としては1週間以内が推奨されており、遠すぎるとユーザーがその間にアプリを開いた際にタスクが起動しない場合がある
    request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
    
    do {
        try BGTaskScheduler.shared.submit(request)
    } catch {
        print("Could not schedule app refresh: \(error)")
    }
}
```

**BGProcessingTaskRequestの場合**

```swift
let request = BGProcessingTaskRequest(identifier: taskIdentifier)

// 通信が必要な場合はtrueにする（デフォルトはfalseで、この場合通信がない時間にも起動される）
request.requiresNetworkConnectivity = true

// 充電中に実行したい処理の場合はtrueにする
// これがtrueの時にCPU Monitorが無効になる
request.requiresExternalPower = true

do {
    try BGTaskScheduler.shared.submit(request)
} catch {
    print("Could not schedule database cleaning: \(error)")
}
```

### 注意点
※ BGTaskRequestは1度の起動にしか対応していない。
そのため、実行後に自動で次のタスクをスケジューリングしたい場合は、タスク実行完了前に再度スケジューリングしておく必要がある。

```swift
BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier, using: nil, launchHandler: { task in
    self.schedule()

    // タスクが実行された時の処理
    // ...
})
```

# デバッグ

**タスクを起動させる**

1. 一度アプリを起動させてタスクをスケジューリングさせる
1. タスクのスケジューリングを行ったあと、Xcodeの一時停止ボタンをクリックする（フォアグラウンドで行う必要があった）
1. LLDBで `e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"TASK_IDENTIFIER"]` を打ち込む
1. 一時停止を解除する
1. タスクが実行される

**タスクを失効させる**

1. Xcodeの一時停止ボタンをクリックする
1. LLDBで `e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"TASK_IDENTIFIER"]` を打ち込む
1. 一時停止を解除する
1. タスクが失効される


# 検証

### 準備

シミュレーターでBGTaskSchedulerのsubmitメソッドを呼び出すと、以下のエラーが発生し挙動を確認できなかった。

```bash
Could not schedule app refresh: Error Domain=BGTaskSchedulerErrorDomain Code=1 "(null)"
```

[ドキュメントに](https://developer.apple.com/documentation/backgroundtasks/starting_and_terminating_tasks_during_development) 、デバッグは実機のみ可能との記述があったので、検証は実機で行った。
> The debug functions work only on devices.

### 結果 （AppRefreshTasks）

- LLDBのデバッグコマンドでの動作検証
    - タスクの実行
        - `_simulateLaunchForTaskWithIdentifier` コマンドを実行すると、 `launchHandler` が呼び出される挙動を確認できた。
        - `launchHandler` 内で次回タスクのスケジューリングを行った場合は、続けてデバッグコマンドを打ち込んでも再度ハンドラが呼び出された
            - 追加のスケジューリングを行わなかった場合は、ハンドラは呼び出されなかった
    - タスクの失効
        - 30秒を超える処理を `launchHandler` 内で行っても処理が完了できた
            - デバッグはアプリをフォアグラウンドにしておかないとできないため、これが原因の可能性がある
            - バックグラウンド状態にしてデバッグコマンドを打つと、Xcodeがフリーズして以降のデバッグを行うことができなくなった
        - 失効用のコマンドを実行した場合は、 `expirationHandler` が呼び出されるのを確認できた
            - この時、一度 `_simulateLaunchForTaskWithIdentifier` を呼び出してタスクを起動状態にしておかないと、失効のハンドラは呼び出されなかった
- 実機での動作検証
    - アプリをバックグラウンド状態にしている際に、AppRefreshTasksが実行されることを確認できた
    - アプリをkillした場合はAppRefreshTasksが実行される挙動は確認できなかった


### 検証 （ProcessingTasks）

- LLDBのデバッグコマンドでの動作検証
    - AppRefreshTasksと同様の結果となった
- 実機での動作検証
    - 指定した条件にしたあと、すぐに実行はされなかった
    - スケジューリング後、2日目あたりからタスクが実行されるようになった
    - 実行されはじめた後は、1日に2回以上動作することもあった
        - （アプリの利用頻度に応じて実行回数は変わりそう）

### 参考

- https://developer.apple.com/documentation/backgroundtasks
- https://developer.apple.com/documentation/backgroundtasks/bgtaskscheduler
- https://developer.apple.com/documentation/backgroundtasks/bgtaskscheduler/3142252-submit
- https://developer.apple.com/documentation/backgroundtasks/refreshing_and_maintaining_your_app_using_background_tasks
- https://developer.apple.com/documentation/backgroundtasks/starting_and_terminating_tasks_during_development