ハイライト
iOS 26 では
BGContinuedProcessingTaskが導入され、ユーザーが明示的に開始した長時間タスク(動画書き出し、SNS への投稿、アクセサリのアップデートなど)を、app がバックグラウンドへ移行したあとも継続して実行できるようになりました。進捗 UI とキャンセル導線はシステム側が提供します。
主要内容
ユーザーが app 内で「4K 動画を書き出す」をタップし、ホームボタンでホーム画面へ戻る。これまでこのシナリオでは、開発者は beginBackgroundTask で猶予時間を確保しても数十秒で suspend され、書き出しが失敗していました。あるいは、ユーザーをフォアグラウンドに引き戻して進捗バーを眺めさせるしかありませんでした。どちらも体験とは呼べません。
iOS 26 はここに第三の道を用意しました。Apple の Ryan は session でこう説明しています。app はバックグラウンドへ移行するとデフォルトで suspend され、CPU 時間は割り当てられません。これはバッテリーを保護し、フォアグラウンドの滑らかさを維持するためです。バックグラウンド実行は権利ではなく、システムが「効率的・最小限・復元可能・行儀よく・適応的」という 5 つの原則に従って配分する有限なリソースです(01:02)。この前提のうえで新たに加わった BGContinuedProcessingTask は、特定のシナリオに特化しています。ユーザーが明示的にトリガーし、明確な終了点があり、進捗を定量化できる長時間タスクです。システムはバックグラウンドで UI を表示し、タイトル・サブタイトル・進捗バーを見せ、ユーザーはいつでもキャンセルできます。Journal app の書き出し機能は、この API で動いています(11:40)。
新しい API は既存 API を置き換えるものではなく、iOS のバックグラウンド実行ツールボックスに残っていた最後のピースを埋めるものです。コンテンツのプリフェッチには BGAppRefreshTask、サーバー側からのプッシュ更新には Background Push、オフラインの ML 推論やデータベースのメンテナンスには BGProcessingTask、ファイルハンドルの後始末には beginBackgroundTask、そしてユーザー起点の長時間タスクには BGContinuedProcessingTask を使います。各 API がそれぞれ別の作業プロファイルに対応し、システムはそれを根拠にリソースを割り振ります。
詳細
app refresh タスクを登録する(08:27)。SwiftUI app では scene modifier で宣言し、システムが利用頻度に応じてスケジュールします。よく使われる app ほどヒット率が高くなります。
import BackgroundTasks
import SwiftUI
@main
struct ColorFeed: App {
var body: some Scene {
WindowGroup {
// ...
}
.backgroundTask(.appRefresh("com.colorfeed.wwdc25.appRefresh")) {
await self.handleAppRefreshTask()
}
}
}
ポイント:
.backgroundTask(.appRefresh(...))は SwiftUI が提供する scene modifier で、逆ドメイン形式の識別子に紐づけます。- クロージャは
asyncで、システムが app をバックグラウンドで起動した際に呼び出されます。クロージャから return すると app は再び suspend されます。 - このルートでは
AppDelegateでregisterする必要はありません。SwiftUI が代わりに行ってくれます。
processing タスクを登録して submit する(09:45)。ML 推論やデータベース整理など、レイテンシに敏感でないバッチ処理向けです。「充電中かつネットワーク接続あり」を必須にすることで、バッテリーへの影響を最小限に抑えられます。
import BackgroundTasks
import UIKit
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.example.apple-samplecode.ColorFeed.db_cleaning",
using: nil
) { task in
self.handleAppRefresh(task: task as! BGProcessingTask)
}
}
func submitProcessingTaskRequest() {
let request = BGProcessingTaskRequest(
identifier: "com.example.apple-samplecode.ColorFeed.db_cleaning"
)
request.requiresNetworkConnectivity = true
request.requiresExternalPower = true
BGTaskScheduler.shared.submit(request)!
}
}
ポイント:
registerは app 起動の早い段階で完了させる必要があります。さもないとバックグラウンドで起こされた際にシステムがハンドラを見つけられず、タスクは即座に失敗します(09:56)。using: nilを指定するとコールバックキューはシステムが選びます。requiresExternalPower = trueで「充電中のみ実行」を宣言でき、バッテリーに優しくなります。requiresNetworkConnectivity = trueでネットワーク必須を宣言します。ネットがないときはシステムが延期します。submit後の実行タイミングはシステムが決定し、即座には走りません。
短時間の後始末には begin/end background task を使う(10:51)。app がバックグラウンドへ移行する直前に追加の実行時間を確保し、状態保存やファイルハンドルのクローズを行うための仕組みです。
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
func saveState() { /* ... */ }
func handlePersistence() {
let app = UIApplication.shared
guard backgroundTaskID != .invalid else { return }
backgroundTaskID = app.beginBackgroundTask(withName: "Finish Export") {
app.endBackgroundTask(self.backgroundTaskID)
self.backgroundTaskID = .invalid
}
self.saveState()
app.endBackgroundTask(backgroundTaskID)
backgroundTaskID = .invalid
}
}
ポイント:
beginBackgroundTask(withName:)は token を返すので、必ず保存しておきます。- クロージャは expiration handler です。システムがタイムアウトと判断したときに呼ばれるので、ここでやることは 1 つだけ、
endBackgroundTaskを呼んで抜けることだけにとどめます。 - 作業が終わったら必ず明示的に
endBackgroundTaskを呼んでください。さもないとシステムが app を強制終了します。 - 「数秒で終わる後始末」向きで、長時間タスクには不向きです。
continued processing タスクを登録する(14:00)。BGProcessingTask と異なり、ここでの register は動的で、ユーザーが意図を表明したあとに呼び出します。
import BackgroundTasks
func handleDialogConfirmation() {
BGTaskScheduler.shared.register("com.colorfeed.wwdc25.userTask") { task in
let task = task as! BGContinuedProcessingTask
var shouldContinue = true
task.expirationHandler = {
shouldContinue = false
}
task.progress.totalUnitCount = 100
task.progress.completedUnitCount = 0
while shouldContinue {
// Do some work
task.progress.completedUnitCount += 1
}
task.setTaskCompleted(success: true)
}
}
ポイント:
- 識別子はあらかじめ Info.plist の
BGTaskSchedulerPermittedIdentifiersに宣言しておく必要があり、bundle ID で始める必要があります。 expirationHandlerではフラグを 1 つ反転させるだけにし、ループ側でフラグを見て優雅に抜けます。ハンドラ内で重い処理をしてはいけません。task.progressは Foundation のProgressReporting Protocol を使います。システムはこれを購読して UI を駆動します。進捗をまったく更新しないタスクはハングと判定されて expire されます(14:44)。- 完了後は必ず
setTaskCompleted(success:)を呼びます。これによりシステムはリソースを回収し、進捗 UI を閉じます。
continued processing リクエストを submit する(15:47):
import BackgroundTasks
func submitContinuedProcessingTaskRequest() {
let request = BGContinuedProcessingTaskRequest(
identifier: "com.colorfeed.wwdc25.userTask",
title: "A succinct title",
subtitle: "A useful and informative subtitle"
)
request.strategy = .fail
BGTaskScheduler.shared.submit(request)!
}
ポイント:
titleとsubtitleはどちらもローカライズが必要です。システム UI にそのままユーザーへ表示されます。strategy = .failは「すぐに開始できないなら失敗とする」を意味します。「いま走らせなければ意味がない」タスクに向いています。デフォルトの挙動はキューに並んで待機する形です。- ワイルドカード識別子(
bundleID.context.*)を使うと、登録時と submit 時に動的なサフィックスを連結できます(13:37)。 - バックグラウンドでの GPU アクセスには Xcode で capability の追加が必要で、ランタイムでは
scheduler.supportedResourcesでデバイスがサポートしているか確認します(17:11)。
重要ポイント
-
「書き出し/アップロード/投稿」にバックグラウンド継続実行を加える: なぜやる価値があるか。ユーザーがもっとも不満を言うのは「戻るを押したら中断された」というケースで、課金につながりやすい機能(動画の書き出し、一括アップロード、アクセサリのファームウェアアップデート)が中断されると最初からやり直しになります。始め方。ボタンの handler 内で
BGContinuedProcessingTaskをregisterし、Progressで既存の進捗ロジックを駆動し、setTaskCompletedを従来の完了コールバックにつなげます。 -
BGProcessingTaskで「夜中に走らせていた処理」を充電中の時間帯へ移す: なぜやる価値があるか。embedding の再構築、ローカルインデックス、キャッシュ整理、ML モデルのウォームアップといった処理をフォアグラウンドで走らせるのはユーザーのバッテリーの無駄遣いで、充電時間帯に寄せれば、システムスケジューラのコスト平準化にも乗れます。始め方。AppDelegateの起動時に identifier をregisterし、リクエスト送信時にrequiresExternalPower = trueとrequiresNetworkConnectivity = trueを付け、あとはシステムのキューに任せます。 -
「app を開いたら古いコンテンツ」を「開いた瞬間に新しいコンテンツ」へ変える: なぜやる価値があるか。フィード系、リーダー系、メール系の app の体感差は、コールドスタートの 1 秒に集約されがちです。始め方。SwiftUI の
.backgroundTask(.appRefresh(...))modifier で軽量なプリフェッチクロージャを登録し、サーバー側の Background Push と組み合わせて補完します。前者は利用頻度ベース、後者はイベントベースで、二本立てで戦います。 -
タスクには常に進捗を持たせる。持たせなければ kill される: なぜやる価値があるか。
BGContinuedProcessingTaskは「システムから見てハングしている」と「システムから見て働いている」を明確に区別します。Progressを更新しないと expire されてしまいます。始め方。既存の進捗コールバックをtask.progress.completedUnitCountに接続します。推定値であっても、まったく報告しないよりは大幅にマシです。同時に、expirationHandler用に即座に抜けられるチェックポイントを用意しておきます。
関連セッション
- Deliver age-appropriate experiences in your app — Declared Age Range API により、app が年齢層に応じた適切なコンテンツを提供できるようになります。
- Filter and tunnel network traffic with NetworkExtension — NetworkExtension フレームワークによるフィルタリングとトンネリング機能。システムレベルのネットワーク介入シナリオを広くカバーします。
- Get ahead with quantum-secure cryptography — CryptoKit に耐量子アルゴリズムを取り込み、向こう 10 年のデータを守ります。
- Optimize home electricity usage with EnergyKit — EnergyKit により、app が家庭の電力網状況に合わせてエネルギー消費タスクをスケジューリングできます。
コメント
GitHub Issues · utterances