Background URLSession

# この記事は

iOSで利用可能なバックグラウンド処理の1つである「URLSessionのバックグラウンドモード」 についてのまとめと挙動の検証です。

# 機能概要

### Background URL Session

- 特徴
    - URLSessionの実行モードの1つ
    - 通信を開始した後バックグラウンドに移行しても、継続して通信処理を実行させ続けることができる
    - アプリがフォアグラウンドのままでも処理を実行することはできる
- 実行タイミング
    - タスクの実行命令を出した直後
- 実行可能時間
    - 環境によって変わる（明記されている公式ドキュメントを見つけることができなかった）
        - `UIApplication.shared.backgroundTimeRemaining` で取得できる
        - 検証環境（iPhone11Pro iOS13）では30secだった
- 所感
    - [Backgorund Task Completion](https://qiita.com/chocoyama/items/8162bf67e452e41b574b) を使ってもある程度同じようなことは実現できるので、正直あまり使い所が思い浮かばなかった
    - 後述する `Discretionary Background URL Session` の方が使い所がありそう

### Discretionary Background URL Session

- 特徴
    - Background URL Sessionの実行オプションの1つ
        - URLSessionConfigurationの `isDiscretionary` をtrueに指定することで、このモードにすることができる
    - 通信の開始タイミングを遅延させることができるので、即時で必要でない処理を先延ばしできる
    - 実行リクエストはフォアグラウンドで行うが、タスクの実行自体はアプリとは別のバックグラウンドプロセスで行われる
    - アプリが停止状態の時に処理が完了しても、システムがバックグラウンドでアプリを再開または起動してくれる
        - この挙動を実現するには `sessionSendsLaunchEvents`がtrueになっている必要がある
        - ※ ユーザーによって明示的にアプリがkillされていた場合は実行されない
- 実行タイミング
    - 正確に指定することはできず、ある程度の実行開始条件を事前に与えておくことしかできない
    - 与えられた条件を考慮して、システムが最適なタイミングを判断して処理を実行する
- 実行可能時間
    - Background URL Sessionと同じ
- 所感
    - アプリがフォアグラウンド状態の時に、システムリソースを消費させてまで行いたくない処理を実行するのに適している
    - 確認した限りでは簡単にデバッグする方法は特に提供されていなさそうだった
        - 「スケジューリングしたあと実行されるのを待つ」しかなさそうなので、テストが辛そう


# サンプルコード

https://github.com/chocoyama/BackgroundSamples/blob/master/BackgroundSample/Views/URLSessionView.swift
https://github.com/chocoyama/BackgroundSamples/search?q=handleEventsForBackgroundURLSession&unscoped_q=handleEventsForBackgroundURLSession

`URLSessionDownloadTask` を利用しています

## Background URL Session の場合

バックグラウンドのセッションを作成・実行する。この時、クロージャではなくdelegateで設定を行う。

```swift
let config = URLSessionConfiguration.background(withIdentifier: UUID().uuidString)
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)

session.downloadTask(with: URLRequest(url: url)).resume()
```

ダウンロード処理が完了すると、下記のDelegateメソッドが呼び出される。
ダウンロードされたデータは、引数に受け渡される `location` のURLに配置されたファイルから参照することができる。
このデータはメソッドの終了と共に利用できなくなるので、メソッド外でも利用したい場合は別のファイルに退避させるなどの対応が必要になる。

```swift
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
    let jsonString = try! String(contentsOf: location)
    NotificationHelper.postLocalNotification(with: Message(body: jsonString))
}

```


## Discretionary Background URL Session の場合

実行が遅延されるため、実行時にはすでにアプリが停止されている可能性がある。
そのため、フォアグラウンド時に生成したセッションがすでに失われている可能性があり、セッションを再生成しないとリクエスト時に設定していたdelegate処理を実行することができない。
※ 処理自体はバックグラウンドの別プロセスで実行されているので、正しくセッションの復帰を行えば、設定したdelegate処理を実行させることができる。

以下の対応を行うことで、通信完了時などに呼び出されるdelegate処理をバックグラウンド時でも実行させられる。

1. URLSessionの復帰
2. システムへの復帰処理完了通知


### 1.URLSessionの復帰
UIApplicationDelegateには、 `handleEventsForBackgroundURLSession` というメソッドが定義されている。
このメソッドはバックグラウンドで通信処理が完了した後、システムがアプリを起動して呼び出すもの。
引数としてセッションIDが受け渡されるので、これを用いて再度URLSessionを起動してセッションの再生成を行うことができる。

※ URLSessionの再生成処理は必ずしもこのメソッド内で行う必要はない。
別の起動ロジックの中で生成時と同一のSessionIDでURLSessionを起動している箇所があれば、そちらで再生成を担保することも可能。

```swift
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
    self.backgroundCompletionHandler = completionHandler
    
    // 必要に応じてここで再生成する
    // let config = URLSessionConfiguration.background(withIdentifier: "some unique identifier")
    // let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
}
```

また、ここで受け渡される `completionHandler` は、実行後にURLSessionのdelegateメソッド（didFinishDownloadingToLocation など）が呼び出されることになる。
そのため、URLSessionの再生成処理が終わった段階で呼び出す必要がある。
別のクラスなどでURLSessionの再生成を行っている場合は、一旦プロパティなどに保持しておくことで後からこの完了ハンドラを呼び出せるようする。
その後、再生成が完了したタイミングで呼び出すことで、想定した動作にすることができる。

### ２. システムへの復帰処理完了通知

特定のURLSessionに関する全てのイベントが実行されたあとは、 `NSURLSessionDelegate` の `urlSessionDidFinishEvents` が呼び出される。
このタイミングではURLSessionの再生成処理が終わっているので、保持しておいた `handleEventsForBackgroundURLSession` の完了ハンドラを実行する。
完了ハンドラが呼び出されたあとは `didFinishDownloadingToLocation` が呼び出されるので、通常のBackgroundURLSessionと同一の処理を実行すれば良い。

```swift
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    DispatchQueue.main.async {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
        appDelegate.backgroundCompletionHandler?()
        appDelegate.backgroundCompletionHandler = nil
    }
}
```

# 検証

## 準備

検証環境
`iPhone11Pro iOS13.3.1 (実機)`
`iPhone11ProMax iOS13.3.1 (Simulator)`

ローカルに簡易的なAPIサーバーをたてて重たいAPIをシミュレートしながら検証を行った。

```js
// Express
router.get('/', function(req, res, next) {
  setTimeout(() => {
    res.send(JSON.stringify({'name': 'SampleName'}));
  }, 10000);
});
```

## 結果

↓のサンプルコードで実行した結果です
https://github.com/chocoyama/BackgroundSamples/blob/master/BackgroundSample/Views/URLSessionView.swift

### フォアグラウンドモードで通信開始

- 実行直後にバックグラウンドに移行した場合、バックグラウンド状態では処理が停止された
- ただし、すぐにフォアグラウンドに復帰させると、中断されていた処理が再開する挙動になった

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41486/b747d03a-d621-7437-143f-d97226443128.gif"/>

### バックグラウンドモードで通信開始

- 実行直後にバックグラウンドに移行しても、バックグラウンド状態で処理が継続された

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41486/ab257ff0-8bbc-4e2f-1ce8-5c5294635a6d.gif"/>


### バックグラウンドモード & isDiscretionary=trueで通信開始

- 実行直後にバックグラウンドに移行した時、処理が遅延されたことが確認できた（すぐに実行されなかった）
- バックグラウンドに移行後、フォアグラウンドに復帰しても処理は実行されなかった
- スケジューリングをしたあと、しばらく待つと通信処理が実行されたことが確認できた（実機検証）

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41486/ce3dc535-cfab-d76f-dda5-c5bc5148b713.gif"/>


# 参考

- https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_in_the_background
- https://developer.apple.com/documentation/foundation/nsurlsessionconfiguration/1411552-discretionary
- https://developer.apple.com/documentation/foundation/urlsessionconfiguration/1407496-background
- https://developer.apple.com/documentation/foundation/urlsessionconfiguration/1617174-sessionsendslaunchevents
- https://academy.realm.io/jp/posts/gwendolyn-weston-ios-background-networking/