アイリッジ開発者ブログ

アイリッジに所属するエンジニアが技術情報を発信していきます。

便利になった!? Background Tasks のデバッグについて

はじめに

こんにちは。開発部第1グループの西岡です。

WWDC 2025 と猛暑で寝不足がちな6月に入りました。

今回は Background Tasks フレームワークを使ったバックグラウンド処理の実装に対するデバッグ方法について触れたいと思います。

なぜ今さらデバッグなのか?

バックグラウンド処理に関する仕組みはiOS13で大きな変化がありました。

近年 Apple Intelligence や Live Activity が注目されたり、開発レベルでは SwiftUI や Swift Concurrency へとパラダイムシフトしています。そんな中でバックグラウンド処理に関連するAPIでも GPUを使ったタスク処理(例: 機械学習や画像処理など)や Live Activity向けの継続的なプログレス表示タスクなども追加されているようです。
ここでは詳細を割愛しますが、つまるところ...

バックグラウンド処理に関連するAPIも地味にアップデートされているのです 😂

そうなると実際に新しいAPIを使ってコードを書いて「さあ動かしてみよう!」と思うわけなのですが、

実は Xcode の Debug メニューから「Simulate Background Fetch」を選択し、タスクを擬似的に発生させようにも、うんとも、すんとも、Debug Areaにも何も出力されません。

Background Tasks では以前の方法が通用しません。

BackgroundTasks 登場前は?

ひと昔前のバックグラウンド処理といえば、iOS7に追加された機能の1つ Background Fetch (〜iOS13)でした。

プロジェクトの Capabilities の Background Modes で 「Background fetch」 という項目にチェックを入れて、実行処理を AppDelegate の application(_:performFetchWithCompletionHandler:) に記述していたのが記憶に新しいですね。

そして、擬似的にBackground Fetchを発生させるためには2通りの方法が紹介されていました。

その1. スキームを分けてビルドのオプションでアプリ終了状態からの Background Fetch を試す

Edit Scheme...

その2. Xcode の Debug メニューから "Simulate Background Fetch" を実行する

Xcode > Debug > Simulate Background Fetch

当時、これだけで実装後の動作検証やデバッグは手軽に実行させることができました。

現在のBackground Tasksとは?

iOS13から Background Tasks というフレームワークが登場しました。
現在はこのフレームワークを使ってバックグラウンド処理を行うよう推奨されています。

developer.apple.com

まずは、Background Tasksでタスク登録〜実行処理までの流れを振り返ってみましょう。

Capabilities設定

Background Modesを有効化し、「Background fetch」または「Background processing」にチェックを入れます。
特に重い処理や通信が長くなるケースでは “Background processing” を推奨されます。

Info.plistファイル

「Background fetch」または「Background processing」に応じて、バックグラウンドタスク用のIdentifierを指定します。

<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
    <string>com.example.refresh</string>
    <string>com.example.processing</string>
</array>

このIdentifierは非常に重要です。なぜなら、バックグラウンド処理のデバッグ実行時に必要になるからです。

タスク登録(実装)

最も一般的な実装方法(AppDelegate)で登録する場合、下記のようになります。

import BackgroundTasks
import UIKit

class AppDelegate: NSObject, UIApplicationDelegate {
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        // BGタスクを登録
        BGTaskScheduler.shared.register(forTaskWithIdentifier: "TASK_IDENTIFIER", using: nil) { task in
            // 次回の再スケジューリング
            scheduleAppRefresh()
            // タスクが実行された時の処理
            handleAppRefresh(task: task as! BGAppRefreshTask)
        }
        
        return true
    }

    func scheduleAppRefresh() {
        // ...
        let request = BGAppRefreshTaskRequest(identifier: "TASK_IDENTIFIER")
        // ...
        try? BGTaskScheduler.shared.submit(request)
    }
    
    func handleAppRefresh(task: BGAppRefreshTask) { 
        // ...
        task.setTaskCompleted(success: true)
    }
  • アプリ起動直後にタスクを登録しています。
  • タスクが発火すると BGTaskScheduler.shared.register() メソッドのクロージャー内が実行されます。
  • タスク終了間際にsetTaskCompleted(success:)をコールし、BGTaskSchedulerに対してタスク完了を知らせます。何らの理由でタスクが未完全なままで中断された場合、引数はfalseで渡します。

また、iOS 16 以上が対象であれば SwiftUI の .backgroundTask(_:action:) を選択することもできます。 developer.apple.com

import BackgroundTasks
import SwiftUI

@main
struct SampleWidgetApp: App {

    WindowGroup {
        ContentView()
    }
    .backgroundTask(.appRefresh("TASK_IDENTIFIER")) { _ in
        // 次回の再スケジューリング
        scheduleAppRefresh()
        // タスクが実行された時の処理
        handleAppRefresh()
    }

    func scheduleAppRefresh() {
        // ...
        let request = BGAppRefreshTaskRequest(identifier: "TASK_IDENTIFIER")
        // ...
        try? BGTaskScheduler.shared.submit(request)
    }

    func handleAppRefresh() { 
        // ...
    }

}
  • .backgroundTask(_:action:) のクロージャー内の処理を抜けると、システムからAppに割り当てられたバックグラウンドタスクは暗黙的に完了し、システムはAppを再び停止します。
  • Swift Concurrencyを使うことで作業完了後の明示的なコールバックは不要です。

ここでは2パターンのタスク登録について説明しましたが、実開発では要件や目的などに即してどちらか一方を実装すれば十分です。

もし同じタスクIDを重複で登録しようとすると強制終了する原因となるため、ここは要注意です。

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Launch handler for task with identifier IDENTIFIER has already been registered’

どうやってデバッグするか?

早速、バックグラウンド状態でタスクを発生させてみましょう。

まずは前準備としてアプリをバックグラウンド状態にしましょう。

前準備

  1. アプリを起動する。
  2. タスクのスケジューリング処理(BGTaskScheduler.shared.register())を通す。
  3. バックグラウンド状態に遷移する。

実行する

ここからが本題です。

バックグラウンドタスクをシミュレートするには、LLDBのプロンプトを使ってタスク実行を命令していきます。

  1. アプリはバックグラウンド状態のままで XcodeのDebug Area を表示し、一時停止ボタン(⏸️)をクリックする。
  2. LLDBで以下を打ち込む。

入力フォーマット

e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"TASK_IDENTIFIER"]

TASK_IDENTIFIER には、先ほどの Info.plistファイルで設定したバックグラウンドタスク用のIdentifier (※1)を指定して下さい。 もし複数のタスクIDを登録している場合には、実行したいタスクIDを指定すればOKです。

Debug Areaの様子

// LLDB実行
(lldb) e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"TASK_IDENTIFIER"]

Simulating launch for task with identifier TASK_IDENTIFIER // ここが出力されたら準備完了です!!

あとはXcodeの一時停止ボタンを解除すれば、バックグラウンドで1度だけタスクが開始されます。(※2)

※1. 手順2で指定する TASK_IDENTIFIER のバックグラウンドタスク用のIdentifierは、タスク登録で指定したIDと同じものです。
※2. また、再度呼び出したい場合は、バックグラウンド状態のままで手順1-2 を繰り返せばタスクを実行できます。

【要注意】タスクが未完了状態のとき、手順1-2を行うと強制終了します。

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'No launch handler registered for task with identifier {public}@'

おわりに

最近、とある案件でiOSのバックグラウンド処理について検証する機会がありました。 iOS 13 が登場して以降、まだ Background Tasks を使ってソースコードを書いて動かしたことがなく、もちろん旧バックグラウンドフェッチ時代のことも頭からすっぽりと抜け落ちていたため、まずはリハビリがてらデモアプリを作ってみました。

一連の実装はさほど難解ではなく、ゼロからであっても Apple が公開しているサンプルコードは非常に参考になります。ただし、動作確認となると、バックグラウンド状態はフォアグラウンド状態と比べて制約が多く、挙動も気難しいものがあります。今回紹介した方法は Apple が公式に案内しているもののひとつですが、それ以外の手段となると、現状ではログ出力して Console で確認するのが唯一の方法なのかもしれません。

一方で、複数タスクを ID で登録・管理できる仕組みが導入されたことで、バックグラウンド処理をタスク単位でデバッグ・検証できるようになったのは大きな進化です。 デバッグ手順は一見すると不便になったように思えますが、LLDB という強力なデバッグツールを活用できる点は見逃せないメリットだと感じました。

ご参考になれば幸いです!

APPBOXはお知らせ配信以外にもバックグラウンド処理を活用した機能もあります。ご興味があれば是非チェックしてみてください!

おすすめ記事

本ブログではモバイルアプリ開発に関する技術やエンジニアの体験談などを発信しています。 こちらもぜひご覧ください!

iridge-tech.hatenablog.com iridge-tech.hatenablog.com

References