はじめに
この記事は、表題の通り、AppRefreshTasksを利用する際に、CoreDataのエンティティ取得など、データベース操作を行う際に自身がハマってしまったところを記載し、忘備録として記したものです。
理由など、突き詰めて記載できていませんが、他にドンピシャな記事がなく、自分自身色々時間がかかってしまったので、同じように躓いているどなたかの参考になればと思います。
AppRefreshTasksとは
バックグラウンドでの処理のことです。
普通にバックグラウンドでの処理を思い浮かべると、アプリがフォアグラウンドの状態からホーム画面に移動した際の、バックグラウンド時ことかと考えると思いますが、このAppRefreshTasksは事前にOSに実行タイミングを登録しておき、定期的に処理することをいいます。
以下に、他の方が詳しく記載されていますので、ご参考にどうぞ。
1. https://developer.apple.com/documentation/backgroundtasks
2. BackgroundTasks(AppRefreshTasks & ProcessingTasks)
3. https://grandbig.github.io/blog/2019/09/22/backgroundtasks/
本題
さて、本題ですが、このAppRefreshTasksの処理内でCoreDataのデータベースを参照したいな思った際は、大体以下のようなコードを記載するかと思います。
まずは、スケジュールの設定
private func scheduleAppProcessing() {
let request = BGProcessingTaskRequest(identifier: "com.Sample.refresh")
request.requiresNetworkConnectivity = false
request.requiresExternalPower = true
do {
// スケジューラーに実行リクエストを登録
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule app processing: \(error)")
}
}
func applicationDidEnterBackground(_ application: UIApplication) {
// バックグラウンド起動に移ったときにルケジューリング登録
scheduleAppProcessing()
}
続いて、Core DataはDAOでこんなように実装している方は多いのではないでしょうか?
import Foundation
import CoreData
import UIKit
class DataDAO {
/**
CoreDataとのコネクション
*/
private var manageObjectContext: NSManagedObjectContext
init() {
self.manageObjectContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
}
func getAll() -> [Data} {
・・・
return dataList
}
}
上記の状態で、実際のAppRefreshTasksの処理を記述するとこんな感じかと、
static func handleAppRefresh(
task: BGAppRefreshTask,
) async {
// バックグラウンド処理が途中で打ち切られた場合
task.expirationHandler = {
// 予約されているタスクの確認
BGTaskScheduler.shared.getPendingTaskRequests { requests in
if requests.count == 0 {
// 当日中の再バックグラウンド処理をシステムに登録
self.schedule(isCancel: true)
}
}
}
// 処理したいもの
let dataList = DataDAO().getAll()
・・・
// 再スケジューリング
self.schedule()
task.setTaskCompleted(success: true)
}
上のように記述をすると、AppRefreshTasksの処理はバックグラウンドで処理をするため、Main Threadで処理するようエラーメッセージが出力されバックグラウンド処理がクラッシュしてしまいます。
Main Threadで怒られているのは以下の箇所
init() {
self.manageObjectContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
}
エラーコードでググったりするとすぐ出てくるが、Core Dataをバックグラウンドで扱うときには、
performBackgroundTask(_:)
や
newBackgroundContext()
これらを使用するようにというドキュメントに辿り着くでしょう。
https://developer.apple.com/documentation/coredata/using_core_data_in_the_background
performBackgroundTask(_:)
https://developer.apple.com/documentation/coredata/nspersistentcontainer/1640564-performbackgroundtask
newBackgroundContext()
https://developer.apple.com/documentation/coredata/nspersistentcontainer/1640581-newbackgroundcontext
ではこれらを実際利用する場合、どう使うかというとパッと考えられるのは以下。
import Foundation
import CoreData
import UIKit
class DataDAO {
/**
CoreDataとのコネクション
*/
private var manageObjectContext: NSManagedObjectContext
init() {
self.manageObjectContext = (UIApplication.shared.delegate as! AppDelegate).persistentCloudContainer.viewContext
}
init(context: NSManagedObjectContext) {
self.manageObjectContext = context
}
}
static func handleAppRefresh(
task: BGAppRefreshTask,
) async {
// バックグラウンド処理が途中で打ち切られた場合
task.expirationHandler = {
// 予約されているタスクの確認
BGTaskScheduler.shared.getPendingTaskRequests { requests in
if requests.count == 0 {
// 当日中の再バックグラウンド処理をシステムに登録
self.schedule(isCancel: true)
}
}
}
// -------------
let context = (UIApplication.shared.delegate as! AppDelegate).persistentCloudContainer.newBackgroundContext()
// -------------
// 処理したいもの
let dataList = DataDAO(context: context).getAll()
・・・
// 再スケジューリング
self.schedule()
task.setTaskCompleted(success: true)
}
しかし、これだと結局変わらず、今度はまた同じ箇所でMain Threadエラーを引き起こします。
// -------------
let context = (UIApplication.shared.delegate as! AppDelegate).persistentCloudContainer.newBackgroundContext()
// -------------
こんな状態で、さて、newBackgroundContextを使えと言われたものの、どう使えばいいんだ?状態に陥り、色々試行錯誤してました。
ですが、ある日、悩みながら別のコードをいじっていたところ、ふとなんとなく今の箇所に戻ってきて、なんとなくこんなようなコードを書いたところ、エラーが解消されましたwwww
それはこの箇所
@main
class AppDelegate: NSObject, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
・・・
// 第一引数: Info.plistで定義したIdentifierを指定
// 第二引数: タスクを実行するキューを指定。nilの場合は、デフォルトのバックグラウンドキューが利用されます。
// 第三引数: 実行する処理
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.Sample.refresh", using: .main) { task in
let context = self.persistentContainer.newBackgroundContext()
Task {
// バックグラウンド処理したい内容
await BGAppRefreshTasks.handleAppRefresh(
task: task as! BGAppRefreshTask,
context: context
)
}
}
・・・
return true
}
そうです、AppDelegate.swiftの起動時に設定したバックグラウンドの実行する処理を書くところでした。