はじめに
今回はiOS13で新たに追加された BackgroundTasks Framework
について見ていきたいと思います。
基本的には、 WWDC2019動画の『Advances in App Background Execution』を見ながら実践してみました。
ですが、微妙に躓くところもあったのでメモとして残しておきたいと思います。
BackgroundTasksとは
まず、 BackgroundTasks
の説明です。
BackgroundTasks
はiOS13から利用できる新しいFrameworkになります。
BackgroundTasks
には大きく分けて下記2つのAPIが存在します。
- Background Processing Tasks
- Background App Refresh Tasks
Background Processing Tasks
Background Processing Tasks
は以下シーンでの利用が想定されています。
- (急を要しない)後々の実行で良いメンテナンス処理
- Core MLを利用した機械学習のトレーニング処理
そのため、 数分間 の処理実行が許されています。
また、 requiresExternalPower
というフラグを true
にすることで、CPU消耗によるプロセスキルをさせないよう制御することができます。
(iOSの世界でこれって結構スゴイ気がしますね。)
Background App Refresh Tasks
Background App Refresh Tasks
は以下シーンでの利用が想定されています。
- 30秒以内で完了できる処理
- 1日を通してアプリを最新に保つために必要な処理
これまで上記のような対応をする際には Background Fetch
を利用していたことと思いますが、
今回の新APIの発表により、旧APIはdeprecatedになったそうです。
1
2
3
| // deprecated対象
UIApplication.setMinimumBackgroundFetchInterval(_:)
UIApplicationDelegate.application(_:performFetchWithCompletionHandler:)
|
BackgroundTasksの使い方
続いて具体的な使い方を見ていきます。
ソースコードを書き始めるまでの下準備
① Xccode上でCapabilityを追加します
バックグラウンド処理を利用する場合はこれまで通り Capability
の Background Modes
が必要になります。
Xcode11で追加する方法が少々変わっているので気をつけましょう。
② Background Modesにチェックを入れます
今回は、 Background Processing Tasks
と Background App Refresh Tasks
なので下図の通りです。
③ Info.plistにIdentifierを登録します
Info.plist
に Permitted background task scheduler identifiers
を追加します。
また、 Background Processing Tasks
と Background App Refresh Tasks
用にそれぞれ Identifier
を定義します。
因みに、この Identifier
は例によってユニークであることが求められるので、
com.xxxx.XXXXSample.process
, com.xxxx.XXXXSample.refresh
といったDNSの逆書きが推奨されています。
ソースコードの実装
ここまででソースコード以外の準備は完了です。
続いて、ソースコードを書いていきましょう。
Background Processing Tasks
と Background App Refresh Tasks
それぞれ記載します。
Background Processing Tasks
① BackgroundTasksをimportします
1
2
3
| // AppDelegate.swift
import UIKit
import BackgroundTasks
|
② didFinishLaunchWithOptions内にバックグラウンドタスクを登録します
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // AppDelegate.swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
...
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// 第一引数: Info.plistで定義したIdentifierを指定
// 第二引数: タスクを実行するキューを指定。nilの場合は、デフォルトのバックグラウンドキューが利用されます。
// 第三引数: 実行する処理
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.MeasurementSample.refresh", using: nil) { task in
// バックグラウンド処理したい内容 ※後述します
self.handleAppProcessing(task: task as! BGProcessingTask)
}
return true
}
}
|
③ バックグラウンドタスクにスケジューリングします
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| private func scheduleAppProcessing() {
// Info.plistで定義したIdentifierを指定
let request = BGProcessingTaskRequest(identifier: "com.MeasurementSample.refresh")
// 通信が発生するか否かを指定
request.requiresNetworkConnectivity = false
// CPU監視の必要可否を設定
request.requiresExternalPower = true
do {
// スケジューラーに実行リクエストを登録
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule app processing: \(error)")
}
}
func applicationDidEnterBackground(_ application: UIApplication) {
// バックグラウンド起動に移ったときにルケジューリング登録
scheduleAppProcessing()
}
|
④ 実際に実行する処理を定義します
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| // AppDelegate.swift
// サンプル用のOperation
class PrintOperation: Operation {
let id: Int
init(id: Int) {
self.id = id
}
override func main() {
print("this operation id is \(self.id)")
}
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
...
private func handleAppProcessing(task: BGProcessingTask) {
// 1日の間、何度も実行したい場合は、1回実行するごとに新たにスケジューリングに登録します
scheduleAppRefresh()
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
// 時間内に実行完了しなかった場合は、処理を解放します
// バックグラウンドで実行する処理は、次回に回しても問題ない処理のはずなので、これでOK
task.expirationHandler = {
queue.cancelAllOperations()
}
// サンプルの処理をキューに詰めます
let array = [1, 2, 3, 4, 5]
array.enumerated().forEach { arg in
let (offset, value) = arg
let operation = PrintOperation(id: value)
if offset == array.count - 1 {
operation.completionBlock = {
// 最後の処理が完了したら、必ず完了したことを伝える必要があります
task.setTaskCompleted(success: operation.isFinished)
}
}
queue.addOperation(operation)
}
}
}
|
Background App Refresh Tasks
① BackgroundTasksをimportします
1
2
3
| // AppDelegate.swift
import UIKit
import BackgroundTasks
|
② didFinishLaunchWithOptions内にバックグラウンドタスクを登録します
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // AppDelegate.swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
...
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// 第一引数: Info.plistで定義したIdentifierを指定
// 第二引数: タスクを実行するキューを指定。nilの場合は、デフォルトのバックグラウンドキューが利用されます。
// 第三引数: 実行する処理
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.MeasurementSample.refresh", using: nil) { task in
// バックグラウンド処理したい内容 ※後述します
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
return true
}
}
|
③ バックグラウンドタスクにスケジューリングします
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| private func scheduleAppRefresh() {
// Info.plistで定義したIdentifierを指定
let request = BGAppRefreshTaskRequest(identifier: "com.MeasurementSample.refresh")
// 最低で、どの程度の期間を置いてから実行するか指定
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
do {
// スケジューラーに実行リクエストを登録
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule app refresh: \(error)")
}
}
func applicationDidEnterBackground(_ application: UIApplication) {
// バックグラウンド起動に移ったときにルケジューリング登録
scheduleAppRefresh()
}
|
④ 実際に実行する処理を定義します
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| // AppDelegate.swift
// サンプル用のOperation
class PrintOperation: Operation {
let id: Int
init(id: Int) {
self.id = id
}
override func main() {
print("this operation id is \(self.id)")
}
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
...
private func handleAppRefresh(task: BGAppRefreshTask) {
// 1日の間、何度も実行したい場合は、1回実行するごとに新たにスケジューリングに登録します
scheduleAppRefresh()
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
// 時間内に実行完了しなかった場合は、処理を解放します
// バックグラウンドで実行する処理は、次回に回しても問題ない処理のはずなので、これでOK
task.expirationHandler = {
queue.cancelAllOperations()
}
// サンプルの処理をキューに詰めます
let array = [1, 2, 3, 4, 5]
array.enumerated().forEach { arg in
let (offset, value) = arg
let operation = PrintOperation(id: value)
if offset == array.count - 1 {
operation.completionBlock = {
// 最後の処理が完了したら、必ず完了したことを伝える必要があります
task.setTaskCompleted(success: operation.isFinished)
}
}
queue.addOperation(operation)
}
}
}
|
BackgroundTasksのデバッグ方法
上記で実装が完了しました。
実際に挙動を試すためには、特別な手順が必要になります。
① アプリを起動します
② アプリをバックグラウンドに移します(登録トリガーのためです)
③ 再度アプリを起動します
④ Xcodeで Pause Program Execution
をタップします
⑤ LLDBに以下コマンドを入力して実行します
1
2
3
4
5
| // Background Processing Tasksの場合
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.MeasurementSample.process"]
// Background App Refresh Tasksの場合
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.MeasurementSample.refresh"]
|
⑥ 再度、Xcodeで Pause Program Execution
をタップします
以上で実際の実行処理が見れるようになるはずです。
因みに、実行処理の期限切れを試したい場合は、以下のように⑤のLLDBで以下を入力して実行します
1
2
3
4
5
| // Background Processing Tasksの場合
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.MeasurementSample.process"]
// Background App Refresh Tasksの場合
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.MeasurementSample.refresh"]
|
ハマったところ
筆者が試しに実装してハマったところを参考までに載せておこうと思います。
筆者の場合、なぜかシミュレータで実行しようとすると、以下のエラーが発生してしまいました。
1
| BGTaskSchedulerErrorDomain Code=1 "(null)"
|
これは、サンプルコードで試しても同様でした。
ただ、実機で試したところ問題なく通ったんですよね…
まとめ
さて如何でしたでしょうか。
iOS13で追加された新APIは旧バージョンサポートのため、
すぐには利用されないかもしれませんが、iOS13の普及に伴い、利用シーンは確実に増えていくことでしょう。
そのため、サンプル程度の実装でも、使い方を学んでおくことは今後の役に立つと思っています。
と言ったところで本日はここまで。