SwiftUIアプリでバックグラウンドタスクの実行(ネットワーク、プッシュ通知) (BackgroundTasks, URLSession)

:::note
本記事で使用している画像の一部は、WWDCのセッションビデオより引用しています。
:::

iOS 16の新しいビューモディファイアで、SwiftUIで簡単にバックグラウンドタスクを実行することができるようになりました。

## バックグラウンドタスク (BackgroundTasks) とは

バックグラウンドタスクは、アプリケーションがアクティブに実行されていないときに実行されます。
この記事では、アプリ内のデータを再読み込みする（次の通知やウィジェットの再読み込みなどのスケジュールも設定する）ために使用される、リフレッシュバックグラウンドタスク (.refresh) について説明します。

## 通常のフロー

![schedule_graph.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/5625ad45-3fd5-5d1c-24d9-3bb4dd8acc10.jpeg)

上のグラフで、青い長方形はフォアグラウンドのアプリ、灰色の長方形はバックグラウンドのアプリ、緑はシステムを表しています。

フォアグラウンド (Foreground runtime) のアプリが一時停止すると、正午 (noon) にアプリの更新を要求することができます。正午になると、システム (System) はバックグラウンド (Background runtime) でアプリを起動し、タスクを実行します。このグラフでは、アプリがURLのリモートデータリクエスト (URL Session request) を送信しています。そして、更新タスクのバックグラウンド時間が終了したため、バックグラウンドタスクが終了 (Suspend) します。その後、しばらくして、URLリモートデータリクエストが応答を返すと、システムは再びバックグラウンドのアプリを起動させます (URLSession response)。このとき、アプリはローカル通知を送信する (Send notification) ようにスケジューリングすることができます。

## 通常のユースケース

例えば、このデモプロジェクトはQiita.comのホームページをチェックして、`Swift`キーワードがあるかどうかを確認します。もしあれば、ローカルにプッシュ通知を送ります。

アプリのインターフェイスを更新するためにリモートからデータを取得したり、ユーザーにプッシュ通知を送信するためにリフレッシュタスクを使用することができます。

## 制限事項

リフレッシュバックグラウンドタスクは最大1つまで利用可能です。各バックグラウンドタスクには、実行時間（1分以内）が設定されています。この時間を超えると、システムはそのプロセスを終了させます。

システムのバッテリーが低下したとき、またはユーザーがアプリ・スイッチャーでアプリを閉じたときは、バックグラウンド・タスクは実行されません。

ユーザーがアプリを最初にインストールしたとき、通常は最初のバックグラウンド・タスクをスケジュールします。そして、最初のバックグラウンドタスクが実行され始めると、次のタスクがスケジュールされます。

## エンタイトルメントを追加する

まず、Background Modes (Background fetch, Background Processing)エンタイトルメントを追加します。

![background_task_entitlement.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/64fe3100-6ae6-2cb9-79e5-f87374a75ba5.jpeg)

次に、タスクの識別子を定義します。
`Info.plist`で、`BGTaskSchedulerPermittedIdentifiers`という名前の新しい配列エントリを作成し、識別子の値を設定します。

![bgtask_identifiers.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/99b63463-4410-740b-d6a2-338a0bf829e1.jpeg)

## ユーザーがバックグラウンド再読み込みを許可しているかどうかを確認する

ユーザーは、アプリごとにバックグラウンド再読み込み機能のオン／オフを切り替えることができます。
許可しているかどうかを確認することができます。

```swift
NSProcessInfo.processInfo.backgroundRefreshStatus
```

## バックグラウンドタスクの予約 (スケジュール)

バックグラウンドタスクをスケジュールするには、バックグラウンドタスクが実行される時刻を定義する必要があります。
この時刻はタスクが実行される最も早い時刻 (earliest begin date) を意味することに注意してください。
タスクが実行される正確な時間ではありません。
定義された時間の後、システムがタスクを実行する時間を決定します。

:::note warn
通常、iOS 16 beta 2のテストでは、タスクはスケジュールされた時間の30分後と1時間後に実行されます。
シミュレータでバックグラウンドのアクティビティをテストすることはできませんので、ご注意ください。
:::

```swift
// import BackgroundTasks
if let scheduledTime = Calendar.current.date(byAdding: .second, value: 10, to: Date()) {
    let request = BGAppRefreshTaskRequest(identifier: "com.test.test.appRefreshTest")
    request.earliestBeginDate = scheduledTime
    do {
        try BGTaskScheduler.shared.submit(request)
    } catch {
        // Handle errors here...
    }
}
```

上記のように、まず `BGAppRefreshTaskRequest` オブジェクトを識別子で初期化します。次に、`earliestBeginDate`変数を用いて、最も早い開始日を指定します。
次に、 `BGTaskScheduler.shared.submit(request)` を使ってタスクをスケジュールします。

## バックグラウンドタスクのリストアップ

BGTaskScheduler.shared.getPendingTaskRequests` 関数を使うと、スケジュールされているすべてのバックグラウンドタスクをリストアップすることができます。

```swift
BGTaskScheduler.shared.getPendingTaskRequests { requests in
    DispatchQueue.main.async {
        self.allUpdateRequests = requests
    }
}
```

また、Swiftの新しいasyncを使って、上記のコードを書くことができます。

```swift
Task {
    let allRequests = await BGTaskScheduler.shared.pendingTaskRequests()
    print(allRequests)
}
```

バックグラウンドタスクが実行されると、そのタスクは保留中のリクエストリストから削除されます。
リクエストがまだリストに残っている場合は、システムがまだそのタスクの実行を決定していないことを意味します。

## スケジュールされたタスクの削除

スケジュールされたタスクはすべて削除することができます。

```swift
BGTaskScheduler.shared.cancelAllTaskRequests()
```

また、特定の識別子を持つスケジュールタスクの削除も可能です。

```swift
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: "com.test.test.appRefreshTest")
```

## (>iOS 16 のみ) バックグラウンドタスクのために実行するコードを追加する

SwiftUI アプリの @main コードで、`WindowGroup` ビューのための `.backgroundTask` ビューモディファイアを追加します。

```swift
@main
struct BackgroundTaskSwiftUIDemoApp: App {
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .backgroundTask(.appRefresh("com.test.test.appRefreshTest")) {
            await scheduleTestNotification()
            await DataFetchHelper.shared.startRequestingRemoteData()
        }
    }
    
}
```

ビュー修飾子 `.backgroundTask` を初期化するために、バックグラウンドタスクの種類を定義します。アプリのリフレッシュタスクには `.appRefresh` を、バックグラウンドのURLセッションには `.urlSession` を定義できます。内カッコの中には、識別子を記述します。

### .backgroundTaskビューモディファイアを選択的に適用

.backgroundTask` ビュー修飾子は iOS 16 以降のシステムでのみ利用可能です。
このビュー修飾子をオプションで適用することで、アプリがiOS 16の新機能を試しつつ、iOS 15や他のシステムとの互換性も維持することができます。

```swift
extension Scene {
    func backgroundTaskIfAvailable(_ taskIdentifier: String, action: @Sendable @escaping () async -> Void) -> some Scene {
        if #available(iOS 16.0, *) {
            return self
                .backgroundTask(.appRefresh(taskIdentifier), action: action)
        } else {
            return self
        }
    }
}
```

上記のコードは、iOS 16がインストールされている場合にのみ `.backgroundTask` を適用します。

注意: このビュー修飾子をオプションで適用した場合、古いiOSシステムでもバックグラウンドタスクを実行したいのであれば、バックグラウンドタスクのコードブロックを定義する必要があります (次のセクションを参照)。

## (互換性) 背景タスクを実行するためのコードブロックを定義する

バックグラウンドタスクのコードを古いiOSシステムと互換性のあるものにしたい場合、アプリが起動する前にAppDelegateにブロックを定義します。

```swift
BGTaskScheduler.shared.register(forTaskWithIdentifier: <#T##String#>, using: <#T##DispatchQueue?#>, launchHandler: <#T##(BGTask) -> Void#>)
```

## バックグラウンド・ネットワーク (URLSession)

`URLSessionConfiguration.background` 設定を使用すると、バックグラウンドでのネットワーク呼び出しを行うことができます。ここでは、バックグラウンドでのネットワークリクエストを行うためのヘルパーを作成します。

```swift
import Foundation

class DataFetchHelper: NSObject, URLSessionDownloadDelegate {
    
    static let shared = DataFetchHelper(backgroundTaskIdentifier: "com.test.test.fetchRemoteData")
    
    private var urlSession: URLSession?
    
    init(backgroundTaskIdentifier: String) {
        super.init()
        let config = URLSessionConfiguration.background(withIdentifier: backgroundTaskIdentifier)
        config.sessionSendsLaunchEvents = true
        self.urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)
    }
    
    func startRequestingQiitaWebsite() {
        let request = URLRequest(url: URL(string: "https://qiita.com")!)
        let task = urlSession?.downloadTask(with: request)
        task?.resume()
    }
    
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        if let textData = try? String(contentsOf: location, encoding: .utf8) {
            print(textData)
        }
    }
    
}
```

ネットワーク要求が短時間で完了した場合は、すぐに結果を取得して処理する必要があります。

そうでない場合は、バックグラウンドで作業を続けるためにネットワーク呼び出しを拒否し、`sessionSendsLaunchEvents`を定義したので（ダウンロードが完了したらバックグラウンドアプリが起動するという意味）、ダウンロードイベントが完了したらイベントを受信することができます。

```diff
@main
struct BackgroundTaskSwiftUIDemoApp: App {
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .backgroundTask(.appRefresh("com.test.test.appRefreshTest")) {
            scheduleTestNotification()
            DataFetchHelper.shared.startRequestingQiitaWebsite()
        }
+        .backgroundTask(.urlSession("com.test.test.fetchRemoteData")) { item in
+            _ = DataFetchHelper.shared
+        }
    }
    
}
```

デモプロジェクトを実行すると、以下のようになります。30分ほど待つと、バックグラウンドタスクが実行されます。

![breakpoint_bgtask.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/3e4552d1-d0d6-6e99-8b0c-8f9164fab6f6.jpeg)

ここで、バックグラウンドダウンロードが完了したことがアプリに通知されたら、タスクをダウンロードしたときと同じパラメータでURLSessionを初期化する必要があります。(上記のサンプルコードでは、`DataFetchHelper`を初期化していますが、これも同じように行います)

システムは、それが同じURLセッションであることを認識します。したがって、デリゲート関数 `func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)` は自動的にダウンロード済みのデータで呼び出されるはずです。

古いバージョンのシステムでは、アプリはAppDelegateを通じてバックグラウンドタスクが完了したことを通知されます。

```swift
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void)
```

SwiftUIアプリを使用している場合、AppDelegateアダプタを追加することができます。

## デモコード

Xcode 13を使って、このアプリをデバイス上で実行してみてください。最初のボタンをクリックして、バックグラウンドタスクをスケジュールします。
そして、Xcodeを開き、iPhoneを接続し、debuggerを開いたままにしておきます。
アプリをバックグラウンドにスワイプして、それから待ってください。
約30分後、コンソールにログが表示され、バックグラウンドタスクが実行されたことを示すプッシュ通知が表示されるはずです）。

:::note warn
注意：ブレークポイントを見続ける必要があります。バックグラウンドタスクの実行時間は約1分に制限されているため、ブレークポイントを遅くまで続けると、バックグラウンドプロセスがすでに終了している可能性があります。
:::

https://github.com/mszpro/BackgroundTaskSwiftUIDemo

---


:thumbsup: お読みいただきありがとうございました。

[☺️ Twitter @MszPro](https://twitter.com/MszPro)
[🐘 Mastodon @me@mszpro.com](https://sns.mszpro.com/@me)

:sunny: <img width="150" src="https://cdn.mszmagic.com/static-web-content/AppClipImage_small.png">

---
![writing-quickly_emoji_400.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/640938e9-c121-8152-b8c4-8ab63124afcc.png)

Written by MszPro~

---

# 関連記事

・[UICollectionViewの行セル、ヘッダー、フッター、またはUITableView内でSwiftUIビューを使用（iOS 16, UIHostingConfiguration）](https://qiita.com/MaShunzhe/items/3d98fa897aa1bcffc74b#github-repository)

・[iPhone 14 ProのDynamic Islandにウィジェットを追加し、Live Activitiesを開始する（iOS16.1以降）](https://qiita.com/MaShunzhe/items/6cc8d9d204f40cbee19c)

・[iOS 16：秘密値の保存、FaceID認証に基づく個人情報の表示/非表示（LARight）](https://qiita.com/MaShunzhe/items/07c1e65277a5008106db)

・[iOS16 MapKitの新機能 : 地図から場所を選ぶ、通りを見回す、検索補完](https://qiita.com/MaShunzhe/items/693e6302876707d5684d)

・[SwiftUIアプリでバックグラウンドタスクの実行(ネットワーク、プッシュ通知) (BackgroundTasks, URLSession)](https://qiita.com/MaShunzhe/items/4e0d1032748fe4f398de)

・[WWDC22、iOS16：iOSアプリに画像からテキストを選択する機能を追加（VisionKit）](https://qiita.com/MaShunzhe/items/ed3985d05975bf35292d)

・[WWDC22、iOS16：数行のコードで作成できるSwiftUIの新機能（26本）](https://qiita.com/MaShunzhe/items/0ed36d8a6de7c56b1d34)

・[WWDC22、iOS 16：SwiftUIでChartsフレームワークを使ってチャートを作成する](https://qiita.com/MaShunzhe/items/cae62af8b07f4f474fd8)

・[WWDC22, iOS 16: WeatherKitで気象データを取得](https://qiita.com/MaShunzhe/items/dce45ccdd05c1f70cf54)

・[WWDC 2022の基調講演のまとめ記事](https://qiita.com/MaShunzhe/items/037580d2ed51a5a6e9d9)