ハイライト
SwiftUI が WWDC22 で発表されました
backgroundTaskシーン モディファイアを使用すると、バックグラウンド タスク処理で Swift Concurrency を完全に組み込むことができます。ネストされた完了ハンドラーと手動状態管理に使用されていたバックグラウンド更新コードは、線形非同期/待機書き込みに置き換えられるようになりました。 Session では、荒天写真アプリケーション Stormy を例として、スケジュール設定から実行、完了までの完全なバックグラウンド タスクのライフ サイクルを示します。
主要内容
バックグラウンド タスクは、アプリ開発において常にエラーが発生しやすい領域です。 SwiftUI 以前、開発者は使用していましたBGAppRefreshTaskバックグラウンド更新を処理するには、次を使用します。BGProcessingTask時間のかかる操作を処理するには、どちらも UIKit の完了ハンドラー モードに基づいています。タスクの開始後、状態を手動で管理し、完了ハンドラーを忘れずに呼び出して、タイムアウトのキャンセルを処理する必要があります。ステップを逃すと、システムはアプリの消費電力が多すぎると判断し、今後のバックグラウンド実行の機会を減らします。
WWDC22backgroundTask修飾子はこのパラダイムを変えます。 SwiftUIでハングアップするScene、タスクタイプ識別子と非同期クロージャを受け入れます。システムがアプリを起動することを決定すると、クロージャーが呼び出されます。クロージャーが戻ると、システムは自動的にタスクが完了したとみなします。中間のすべてのロジック (ネットワーク リクエスト、データベースの書き込み、通知のスケジューリング) は、async/await を使用して線形に表現でき、ネストされたクロージャーは不要になります。
セッションで使用されるサンプル アプリ Stormy は、荒天追跡アプリです。そのバックグラウンドプロセスは非常にシンプルです。毎日正午に天気予報 API をチェックし、その日に雨が降ると予測されている場合は、ローカル通知をプッシュして、外出して写真を撮ってアップロードするようユーザーに思い出させます。このプロセスには、古い API の下にネストされた 3 つの完了ステップ (バックグラウンド タスクの取得 -> ネットワークの要求 -> 通知のスケジュール設定) が必要ですが、新しい API を使用する場合は 1 つのステップのみが必要です。backgroundTaskクロージャ、内部の 3 行の await が完了します。
もう 1 つの重要な変更は、クロスプラットフォームの統合です。backgroundTaskwatchOS のバックグラウンド更新とバックグラウンドヘルスデータの更新、ウィジェットのタイムライン更新、iOS/tvOS/Mac Catalyst の一般的なバックグラウンド タスクを処理します。 1 つの API セットですべてのシナリオをカバーし、プラットフォーム固有のコードを削減します。
詳細
バックグラウンドタスクを登録する
バックグラウンド タスクの最初のステップは次のとおりです。Sceneプロセッサを登録します。backgroundTask(_:action:)この修飾子は、タスク タイプ識別子 (文字列) と非同期クロージャの 2 つのパラメータを受け入れます。
@main
struct StormyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.backgroundTask(.appRefresh("com.example.stormy.refresh")) {
await refreshWeatherData()
}
}
}
キーポイント:
.appRefresh("identifier")内蔵されていますBackgroundTask旧バージョンに相当するタイプの1つBGAppRefreshTask、短期的なバックグラウンド データの更新に使用されます。- 識別文字列は Info.plist に存在する必要があります
BGTaskSchedulerPermittedIdentifiersそうしないと、システムはアプリを起動しません。 - クロージャが返されると、システムは自動的にタスクを完了としてマークします - 手動呼び出しは必要なく、実行すべきではありません
setTaskCompleted。 - クロージャがエラーをスローすると、システムはタスクが完了したとみなします (ただし、将来のスケジュールの優先順位に影響する可能性があります)。
バックグラウンド更新をスケジュールする
ハンドラーを登録することは、システムに「この種のタスクを処理できます」と伝えるだけです。実際にバックグラウンド ウェイクアップをトリガーするには、フォアグラウンドでスケジューリング リクエストを送信する必要があります。
import BackgroundTasks
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.example.stormy.refresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 60) // 一小时后
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("调度失败: \(error)")
}
}
キーポイント:
BGAppRefreshTaskRequestのearliestBeginDateこれは、システムがアプリを起動する最も早い時間であり、正確な起動時間ではありません。実際の実行時間は、バッテリーの状態や使用習慣などの要因に基づいてシステムが独自に決定します。- バックグラウンド タスクが実行を開始するたびに、次の更新リクエストを直ちにスケジュールして「チェーン スケジューリング」を形成する必要があります。そうしないと、バックグラウンド リフレッシュ チェーンが中断されます。
BGTaskScheduler.shared.submit繰り返し呼び出すことができます。同じ識別子を持つリクエストが繰り返し送信されてもエラーは報告されず、システムは最新のリクエストのみを保持します。- バックグラウンド タスクの実際の実行は総予算によって制限されており、頻繁なリクエストによって実行数が増加することはありません。
async/await を使用してバックグラウンド ロジックを処理する
バックグラウンドタスクが実行されると、backgroundTaskクロージャはバックグラウンド スレッドで実行されますが、async/await を使用して Swift Concurrency でサポートされている操作を実行できます。
func refreshWeatherData() async {
// 第一步:调度下一次刷新(链式调度)
scheduleAppRefresh()
// 第二步:请求天气数据
guard let weather = await fetchWeather() else { return }
// 第三步:检查是否下雨
guard weather.precipitation > 50 else { return }
// 第四步:推送本地通知
await scheduleRainNotification(for: weather.location)
}
キーポイント:
await fetchWeather()使用できますURLSession.shared.data(from:)非同期バージョンの場合は、バックグラウンド URLSession のデリゲート モードを使用することもできます。バックグラウンド URLSession の利点は、アプリが一時停止された後も、バックグラウンド タスクの時間割り当てを占有せずにダウンロードを続行できることです。- クロージャ内のコードは完全に線形です。最初にスケジュール、次に要求、次に判断、次に通知です。完了ハンドラーのネストはありません。
- 迅速な同時実行性
Task.isCancelledそしてTask.checkCancellation()バックグラウンド タスクのタイムアウト シナリオに自然に適応し、システムはバックグラウンド タスクに制限付きの実行時間 (通常は約 30 秒) を与えます。タイムアウト後、タスクはキャンセルされ、非同期関数がスローされます。CancellationError。 - 手動で電話をかける必要はありません
setTaskCompleted(success:)——クロージャーが正常に返された場合は成功とみなされ、エラーがスローされた場合は失敗と見なされます。
正しいバックグラウンド タスクの種類を選択してください
SwiftUIBackgroundTaskさまざまな使用シナリオに対応する、いくつかの組み込みタイプが提供されています。
| タイプ | 旧APIに対応 | 目的 | 一般的な期間 |
|---|---|---|---|
.appRefresh(_:) | BGAppRefreshTask | 短いデータ更新 | ~30秒 |
.urlSession(_:) | BGURLSessionTask | バックグラウンドファイルのダウンロード | 分 |
.processing(_:) | BGProcessingTask | データベースのクリーニング、インデックス作成 | 分 |
.widgetRefresh(_:) | ウィジェットのタイムライン | ウィジェット データの更新 | システムによって決定される |
// 后台大文件下载
.backgroundTask(.urlSession("com.example.download")) {
// URLSession 配置为 background 模式
// 下载完成后系统唤醒 App,此闭包被调用
await processDownloadedFiles()
}
// 数据库清理(耗时但不需要立即完成)
.backgroundTask(.processing("com.example.cleanup")) {
await database.cleanup()
}
キーポイント:
.urlSessionは特殊なタイプです。実際のダウンロードは URLSession デーモンによってバックグラウンドで行われ、アプリ自体が一時停止されている可能性があります。ダウンロードが完了した (または失敗した) 場合にのみ、システムはアプリを起動し、クロージャを呼び出します。.processingこのタイプは、実行するにはデバイスの電源をオンにする必要があり、データベースの移行やインデックスの再構築などの電力を消費する操作に適しています。- これらすべてのミッションタイプで共有
backgroundTask(_:action:)修飾子のインターフェースは、識別子のプレフィックスのみが異なります。
チェーン スケジューリング: バックグラウンド更新の中断を防止します。
バックグラウンド タスクでよくある落とし穴は、更新チェーンの破損です。その理由は、開発者はアプリがバックグラウンドに入ったときにのみ更新をスケジュールし、その更新が実行された後は再度スケジュールを行わず、チェーンが切断されるためです。
正しいアプローチは、バックグラウンド タスクが開始されるたびにすぐに次のタスクをスケジュールすることです。
func refreshWeatherData() async {
// 立刻调度下一次——不管这次执行成功与否
scheduleAppRefresh()
// 然后是实际的刷新逻辑
await doActualWork()
}
キーポイント:
scheduleAppRefresh()これをクロージャの最初の行に配置すると、後続のロジック エラーまたはタイムアウトが発生した場合でも、次の更新リクエストが確実に送信されます。- フロントデスクでもご利用いただけます
scenePhaseフォールバック戦略として、変更が発生したときのスケジュールを設定します。onChange(of: scenePhase) { newPhase in if newPhase == .background { scheduleAppRefresh() } }。
キャンセルを適切に処理する
バックグラウンド タスクの実行時間はシステムによって制御されます。予算時間を超過すると、システムは現在のタスクをキャンセルします。 Swift Concurrency のキャンセル メカニズムは、この状況を適切に処理します。
.backgroundTask(.appRefresh("com.example.stormy.refresh")) {
scheduleAppRefresh()
do {
try Task.checkCancellation()
let weather = await fetchWeather()
try Task.checkCancellation()
await scheduleRainNotification(for: weather.location)
} catch is CancellationError {
// 任务被系统取消——已完成能完成的,下次继续
}
}
キーポイント:
Task.checkCancellation()キャンセルされた状態でスローされますCancellationError、複数のチェックポイントの間に配置して、早期に終了できます。- こちらもチェック
Task.isCancelledエラーをスローする必要がない場合に使用されるブール値。 - キャンセルは、アプリに問題が発生したことを意味するものではありません。システムがそれ以上の時間を与えないと判断したことを意味します。キャンセル前に完了した作業 (次回の更新スケジュールなど) は引き続き有効です。
重要ポイント
-
やるべきこと: 既存のアプリのバックグラウンド更新ロジックを移行します。
backgroundTask修飾子。 実行する価値がある理由: 古い BGTaskScheduler API では、完了ハンドラーと可変状態を手動で管理する必要があるため、エラーが発生しやすくなります。新しい API は async/await を使用して線形プロセスを実装し、戻り時にクロージャが完了します。このエラーの結果、システムはバックグラウンド実行クォータを削減し、プッシュ通知やデータ同期などのコア機能に直接影響を与えます。 開始方法: アプリのシーンに追加.backgroundTask(.appRefresh("your.id"))、古いコールバック ロジックを async クロージャに移動します。 Info.plist 内BGTaskSchedulerPermittedIdentifiersで識別子を宣言します。フォアグラウンドで呼び出されるBGTaskScheduler.shared.submitリクエストのスケジュール設定。 -
何をすべきか: チェーン スケジュールを実装するために、すべてのバックグラウンド タスク クロージャーの最初の行で次の更新をスケジュールします。 実行する価値がある理由: バックグラウンド タスク チェーンが中断する最も一般的な理由は、実行後に再度スケジュールされないことです。一度壊れると、次回アプリを手動で開くまで、ユーザーのデータは自動的に更新されなくなります。 開始方法:
scheduleAppRefresh()電話がかけられるのは、backgroundTaskクロージャの最上部、 await の前。同時にフロントでもscenePhaseなる.backgroundバックアップとしても一度呼び出されます。 -
対処方法: 大きなファイルのダウンロードを次のように変更します。
.urlSessionバックグラウンドタスクのタイプ。 実行する価値がある理由:.appRefresh実行には約 30 秒しかかかりませんが、大きなファイルのダウンロードには適していません。.urlSessionアプリが一時停止された後もシステム デーモンがダウンロードを継続できるようにし、完了後にタイムアウトやバックグラウンド バジェットを消費することなく、結果を処理するためにのみアプリを起動します。 開始方法: バックグラウンド モードを設定するURLSession、使用.backgroundTask(.urlSession("your.id"))ハンドラーを登録し、クロージャー内で呼び出します。processDownloadedFiles()。 -
内容: バックグラウンド タスクの長い操作の間に挿入します。
Task.checkCancellation()。 実行する価値がある理由: バックグラウンド タスクの実行時間にはシステム クォータがあります。タイムアウト後にタスクはキャンセルされますが、コードがキャンセル ステータスをチェックしない場合、タスクは次の待機ポイントまで実行を継続し、CPU を浪費し、アプリがキャンセル プロトコルに準拠していないとシステムに認識させます。 開始方法: 毎回await前に呼び出されたtry Task.checkCancellation()。キャンセルに失敗した場合のみcatch is CancellationErrorサイレントに戻ります - 保存された進行状況は失われません。
関連セッション
- SwiftUI の新機能 - ナビゲーション、レイアウト、バックグラウンド タスク用の新しい API を含む、WWDC22 での SwiftUI の包括的なアップデート。
- Swift 同時実行性の視覚化と最適化 — Instruments を使用してバックグラウンド タスクの同時実行動作を分析し、アクターの競合とタスクの優先順位の問題を特定します。
- Swift 非同期アルゴリズムの紹介 — Swift 非同期シーケンス ライブラリ、バックグラウンド タスク (ページング プルなど) のストリーミング データ処理シナリオを直接使用できます。
- Swift の新機能 — Swift の言語レベルでの年次アップデートであり、Swift 同時実行の根本的な改善は、バックグラウンド タスクの安定性に直接影響します。
コメント
GitHub Issues · utterances