Skip to main content
  1. Posts/

iOSアプリがバックグラウンドに遷移した後も処理を継続する方法

iOSアプリは、基本的にはアプリがフォアグラウンドの間しか処理を実行できず、アプリがバックグラウンドにいる間は処理が停止します。 しかし、バックグラウンドでも処理を実行するいくつかの方法が用意されています。 その中でも、今回は、アプリがバックグラウンドへ遷移した瞬間からしばらくの間、処理を止めずに継続できる方法 BackgroundTask の使い方をまとめます。

今回のユースケース #

BackgroundTaskは、Background FetchやBackground Processingとは異なり、アプリがバックグラウンドへ遷移する時に、処理の追加時間をシステムに要求するものです。 例えば、ファイルのアップロード/ダウンロード中にアプリの通信を切りたくないとか、データの書き込みを中断させたくないとかの用途になります。

基本的な実装例 #

Appleのドキュメントをもとに実装します。

基本的な流れは、 beginBackgroundTaskを呼び出し、UIBackgroundTaskIdentifierを取得します。 その後、継続したい処理を実行し、処理が終了したら endBackgroundTask を呼び出して処理の終了を通知します。 このメソッドは、UIApplicationのインスタンスメソッドです。

let id = UIApplication.shared.beginBackgroundTask()

// バックグラウンドに遷移した後もしばらく実行したい処理
await viewModel.uploadData()

UIApplication.shared.endBackgroundTask(id)

時間切れに対応する例 #

このBackgroundTaskが実行できる時間はシステムによって決定されます。何度試しても30秒だったので、最大でも30秒のようです。 beginBackgroundTaskを呼ぶときにexpirationHandlerを渡しておくと、時間切れになる時に任意の処理を実行できるようになります。

let uploadTask = createUploadTask()

let id = UIApplication.shared.beginBackgroundTask {
    // expirationHandler: 時間切れになる時に通知される
    uploadTask.suspend()  // 通信処理を止めるなどする
}

let uploadResult = try await uploadTask.value  // Alamofireだとこんな感じ

UIApplication.shared.endBackgroundTask(id)

expirationHandlerが呼ばれるタイミングは、時間切れになる5秒前のようです。 30秒の猶予が与えられていても、25秒くらいで呼ばれました。

ラッパークラスに隠蔽する例 #

UIApplicationのインスタンスメソッドであることからも読み取れるように、これはUIKitに組み込まれている仕組みです。 おそらくはViewControllerで呼び出すイメージなんだと思いますが、UIのレイヤーで実行するのが最適ではないこともあります。 そういう時のために、ラッパークラスを作って対処する方法を考えてみます。

import UIKit

final class BackgroundTask {
    private var id: UIBackgroundTaskIdentifier = .invalid

    func beginBackgroundTask(_ expirationHandler: @escaping (() -> Void)) {
        if id != .invalid {
            endBackgroundTask()
        }

        self.id = UIApplication.shared.beginBackgroundTask {
            expirationHandler()
            self.endBackgroundTask()
        }
    }

    func endBackgroundTask() {
        UIApplication.shared.endBackgroundTask(id)
        id = .invalid
    }
}

このラッパーがあることで、UIKitをimportしなくてもBackgroundTaskが実行できます。

let backgroundTask = BackgroundTask()
let uploadTask = createUploadTask()

backgroundTask.beginBackgroundTask {
    uploadTask.suspend()
}

let uploadResult = try await uploadTask.value

backgroundTask.endBackgroundTask()

アプリがフォアグラウンドに戻った時に、一時停止した処理を再開する例 #

ここまでの実装は、処理を継続できるのは一度きりだったのですが、現実では、アプリは何度もフォアグラウンドとバックグラウンドを行き来します。 その遷移が何度発生しても処理を継続できるように、実装を工夫してみます。

let backgroundTask = BackgroundTask()
let uploadTask = createUploadTask()

// BackgroundTaskを開始
backgroundTask.beginBackgroundTask { uploadTask.suspend() }

// didBecomeActiveNotificationを監視するTask
var resumingTask: Task<Void, Never>? = Task {
    let notification = await UIApplication.didBecomeActiveNotification
    let didBecomeActiveNotification = NotificationCenter.default.publisher(for: notification)

    for await _ in didBecomeActiveNotification.values {
        if Task.isCancelled { return }

        // アプリがフォアグラウンドに戻ったので、再びBackgroundTaskを開始
        // → アプリがまたバックグラウンドに遷移しても、処理を継続できるようにするため
        backgroundTask.beginBackgroundTask { uploadTask.suspend() }
        uploadTask.resume()
    }
}

let uploadResult = try await uploadTask.value

resumingTask = nil  // 監視する必要がなくなったので破棄する
backgroundTask.endBackgroundTask()

こうすることで、アプリがバックグラウンドとフォアグラウンドを行き来しても、処理を継続し続けられるようになります。 Taskのインスタンスの持ち方や破棄のタイミングは、実装によって工夫する必要がありそうです。

URLSessionのBackground Configuration #

通信に限れば、URLSessionには、アプリがバックグラウンドへ遷移しても通信を継続するConfigurationが用意されています。 このConfigurationを使うことで、今回のBackgroundTaskと同様に、30秒程度は通信を継続できるようです( Background URLSession - Qiita )。

background(withIdentifier:) | Apple Developer Documentation

こちらの方は、時間切れのハンドリングにDelegateを必要とします。この点では、BackgroundTaskの方が実装がシンプルに見えます。 しかし、UIKitを必要とせずURLSessionだけで完結できるという点では、Background Configurationを使う方が、整理された気持ち良い実装になりそうです。

(Alamofireの5系では、BackgroundのURLSessionに対応しなくなったので、BackgroundTaskを使う方が簡単かと思います)

まとめ #

アプリがバックグラウンドに行っても処理を継続させられる方法として UIApplication.shared.beginBackgroundTask を呼び出す実装方法をまとめました。 UIKitをimportする必要があるので、Viewのレイヤーで呼んだ方が良さそうですが、ラッパークラスを作れば他のレイヤーでも使いやすくなるかなと思います(設計にもよると思うけど)。 通信に限って言えば、URLSessionのBackgroundモードを使うのが筋が良さそうです。ただし、Alamofireを使っている場合は、今回のBackgroundTaskを使う方が簡単そうです。

参考