【Swift/Core Data】perform/performAndWaitの使い方!newBackgroundContext

この記事からわかること
- Swift/Core Dataでperform/performAndWaitメソッドの使い方
- newBackgroundContextを参照するときの注意点
- マルチスレッド対応
index
[open]
\ アプリをリリースしました /
参考文献:公式リファレンス:Core Data プログラミングガイド〜Concurrency〜
環境
- Xcode:15.0.1
- iOS:17.0
- Swift:5.9
- macOS:Sonoma 14.1
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
newBackgroundContextとは?
func newBackgroundContext() -> NSManagedObjectContext
Core Dataでデータを操作する際に長時間かかる処理や大量のデータを処理する時などメインスレッドで実行したくない(UIをブロックしたくない)場合にはスレッドを分けて処理を実装することも多いと思います。しかしCore Dataはスレッドセーフではないためマルチスレッドでデータを扱う際には異なるスレッドから同一のContext(NSManagedObjectContext)を操作しないように注意が必要になります。Contextを異なるスレッドから参照しないようにする方法はいくつかあるようですがnewBackgroundContext
メソッドもその1つです。
/// メインスレッドで使用するContext
private func makeContext() -> NSManagedObjectContext {
let context = persistentContainer.viewContext
context.automaticallyMergesChangesFromParent = true
return context
}
/// バックグラウンドスレッドで使用するContext
private func makeBackgroundContext() -> NSManagedObjectContext {
let context = persistentContainer.newBackgroundContext()
context.automaticallyMergesChangesFromParent = true
return context
}
newBackgroundContext
メソッドはNSPersistentContainer
の持つメソッドでバックグラウンドスレッド(プライベートキュー)用のContext(NSManagedObjectContext)を作成する役割を持っています。ここで生成されるContext
はメインで使用しているContext
とは別物なので、データを変更しても即座にメインのContext
にマージされないため、適切なタイミングで明示的に変更を保存する必要があります。
またnewBackgroundContext
で生成したContext
からデータ取得や追加を行う際はperform
またはperformAndWait
ブロックの中で操作する必要があるようです。
performメソッド
公式リファレンス:NSManagedObjectContext.performメソッド
func perform(_ block: @escaping () -> Void)
perform
はNSManagedObjectContext
が持つメソッドで指定されたコンテキストでブロック内の処理を実行する役割を持っています。非同期で処理を行い、指定されたコンテキストのスレッドで実行されます。
public func newEntity<T: NSManagedObject>(context: NSManagedObjectContext, completion: @escaping (T) -> Void) {
context.perform {
let entity = NSEntityDescription.entity(forEntityName: String(describing: T.self), in: context)!
let newObject = T(entity: entity, insertInto: context)
completion(newObject)
}
}
performAndWaitメソッド
公式リファレンス:NSManagedObjectContext.performAndWaitメソッド
performAndWait
メソッドはperform
と同じ役割を持っていますが、異なるのは同期的に実行される部分です。そのためブロック内の処理が完了するまで、現在のスレッドがブロックされるのでメインスレッドでの使用には注意が必要です。ただ利点として先ほど完了ハンドラーで受け取っていた結果を返り値として受け取ることができるようになります。
public func newEntity<T: NSManagedObject>(context: NSManagedObjectContext) -> T {
var result: T!
context.performAndWait {
let entity = NSEntityDescription.entity(forEntityName: String(describing: T.self), in: context)!
result = T(entity: entity, insertInto: context)
}
return result
}
マルチスレッドに対応したCore Data管理クラスを実装してみる
マルチスレッドに対応したCore Data管理クラスを実装してみました。DispatchQueue
などを使用してスレッドを変更してアプリがクラッシュしないか試してみてください。至らぬ点やもっとこうした方が良い点がありましたら教えていただけると助かります。
class CoreDataRepository {
static let shared = CoreDataRepository()
private static let persistentName = "MyCoreDataTest"
private lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: CoreDataRepository.persistentName)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
return container
}()
/// メインスレッドで使用するContext
private func makeContext() -> NSManagedObjectContext {
let context = persistentContainer.viewContext
context.automaticallyMergesChangesFromParent = true
return context
}
/// バックグラウンドスレッドで使用するContext
private func makeBackgroundContext() -> NSManagedObjectContext {
let context = persistentContainer.newBackgroundContext()
context.automaticallyMergesChangesFromParent = true
return context
}
/// 新規作成
public func newEntity<T: NSManagedObject>(onBackgroundThread: Bool = false, completion: @escaping (T) -> Void) {
let context = onBackgroundThread ? makeBackgroundContext() : makeContext()
context.perform {
let entity = NSEntityDescription.entity(forEntityName: String(describing: T.self), in: context)!
let newObject = T(entity: entity, insertInto: context)
completion(newObject)
}
}
/// 追加処理
public func insert(_ object: NSManagedObject, onBackgroundThread: Bool = false) {
let context = onBackgroundThread ? object.managedObjectContext ?? makeBackgroundContext() : makeContext()
saveContext(context)
}
/// 削除処理
public func delete(_ object: NSManagedObject, onBackgroundThread: Bool = false) {
let context = onBackgroundThread ? object.managedObjectContext ?? makeBackgroundContext() : makeContext()
context.delete(object)
saveContext(context)
}
/// Contextに応じたSave
private func saveContext(_ context: NSManagedObjectContext) {
guard context.hasChanges else { return }
do {
try context.save()
} catch let error as NSError {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
/// 取得処理:fetchはContextを切り分ける必要がない?
public func fetch<T: NSManagedObject>(completion: @escaping ([T]) -> Void) {
let context = makeContext()
let fetchRequest = NSFetchRequest<T>(entityName: String(describing: T.self))
context.perform {
do {
let fetchedObjects = try context.fetch(fetchRequest)
completion(fetchedObjects)
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
completion([])
}
}
}
}
この管理クラスはGithubに挙げているので参考にしてください。
おすすめ記事:MyCoreDataTest
ご覧いただきありがとうございました。