カテゴリー
iOS Swift

Core Data Stackを理解する

Cocoacastの良記事を勉強することでCore Dataについてより理解をしたので、忘れないように記録しておきます。

Core Dataはデータをdisk(デバイスのストレージ)に記録(persist)することが出来ますが、それはCore Dataのひとつの機能であって全てではなく、Core DataはあくまでObject Graphを管理するものです。

XcodeでUse Core Dataをチェックしてプロジェクトを作成すると、AppDelegateに最低限のコードテンプレートが用意されますが、それが逆にCore Dataを分かりにくくしている、とも言えます。

Core Data Stackの「構成部品」をひとつひとつ自分で組んでみると、全体像がより見えてくると言えます。

Core Data Stackは以下の三つから構成されます。

  • NSManagedObjectModel
  • NSPersistentStore (NSPersistentStoreCoordinator)
  • NSManagedObjectContext

これらを順序立てて組み上げて使うためのclass CoreDataManagerを用意し、必要な場面で使用します。

import UIKit
import CoreData

class CoreDataManager {
  
  private let modelName: String

  // [3]
  public typealias CoreDataManagerCompletion = () -> ()
  private let completion: CoreDataManagerCompletion
  
  // [4]
  public func privateChildManagedObjectContext() -> NSManagedObjectContext {
    let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
    managedObjectContext.parent = mainManagedObjectContext
    managedObjectContext.undoManager = UndoManager()
    return managedObjectContext
  }
  
  // [5]
  public func saveChanges() {
    mainManagedObjectContext.performAndWait{
      do {
        if self.mainManagedObjectContext.hasChanges {
          try self.mainManagedObjectContext.save()
        }
      } catch {
        print("Unable to save changes of Main Managed Object Context")
        print("\(error), \(error.localizedDescription)")
      }
    }
    
    privateManagedObjectContext.perform {
      do {
        if self.privateManagedObjectContext.hasChanges {
          try self.privateManagedObjectContext.save()
        }
      } catch {
        print("Unable to save changes of Private Managed Object Context")
        print("\(error), \(error.localizedDescription)")
      }
    }
  }
  
  public init(modelName: String, completion: @escaping CoreDataManagerCompletion) {
    self.modelName = modelName
    self.completion = completion
    setupCoreDataStack()
    setupNotification()
  }
  
  // [1]
  private lazy var managedObjectModel: NSManagedObjectModel = {
    guard let modelURL = Bundle.main.url(forResource: modelName, withExtension: "momd") else {
      fatalError("can't get momd url")
    }
    guard let mom = NSManagedObjectModel(contentsOf: modelURL) else {
      fatalError("failed to create model from file: \(modelURL)")
    }
    return mom
  }()
  
  private lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
    let psc = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
    // adding persistentStore in a background thread in addPersistentStore() below
    return psc
  }()

  // [3]
  private func addPersistentStore(to persistentStoreCoordinator: NSPersistentStoreCoordinator) {
    let dirURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last
    let fileURL = URL(string: "\(modelName).sqlite", relativeTo: dirURL)
    do {
      try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: fileURL, options: [
        NSMigratePersistentStoresAutomaticallyOption: true,
        NSInferMappingModelAutomaticallyOption: true
      ])
    } catch {
      fatalError("error configuring persistent store: \(error)")
    }
  }
  
  private func setupCoreDataStack() {
    guard let persistentStoreCoordinator = mainManagedObjectContext.persistentStoreCoordinator else {
      fatalError("unable to set up Core Data Stack")
    }
    // [3]
    DispatchQueue.global().async {
      self.addPersistentStore(to: persistentStoreCoordinator)
      DispatchQueue.main.async {
        self.completion()
      }
    }
  }
  
  private func setupNotification() {
    let notificationCenter = NotificationCenter.default
    notificationCenter.addObserver(self, selector: #selector(onNotifyBackground), name: UIApplication.willResignActiveNotification, object: nil)
    notificationCenter.addObserver(self, selector: #selector(onNotifyBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
  }
  
  @objc private func onNotifyBackground(_ notification: Notification) {
    saveChanges()
  }

  // [2]
  private lazy var privateManagedObjectContext: NSManagedObjectContext = {
    let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
    managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator
    return managedObjectContext
  }()
  
  // [2]
  public private(set) lazy var mainManagedObjectContext: NSManagedObjectContext = {
    let managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
    managedObjectContext.parent = privateManagedObjectContext
    return managedObjectContext
  }()
}

[1] extensionがmomdとなっていますが、これはxcdatamodeldを元にコンパイルされた、より小さな(効率の良い)ファイルです。

[2] NSManagedObjectContextは動作するスレッド(メインスレッドもしくはプライベート(バックグラウンド))を指定出来ます。stack上にcontextは複数あってもよく、例えばdiskに記録するなどの作業はメインスレッドで行われるとUIをブロックしてしまう可能性があるため、バックグラウンドで行うようにするなどの目的別に分けることが出来ます。

[3] persistentStoreCoordinatorにpersistentStoreを追加する際は、状況により少し時間がかかる場合があるため、処理をバックグラウンドで行い、処理完了後の作業をコールバック関数内で行えるようにしています。

[4] mainManagedObjectContextをparentとするprivateChileManagedObjectContextを用意し、UIViewControllerなで使用するためのもの。

[5] 各contextはsave( )する事に、自身とparentに保存内容をpushします。mainManagedObjectContextがsave( )し終わるのを待ち、その後privateManagedObjectContextがsave( )し、diskに記録されるという流れです。