| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299 |
- //
- // PersistenceController.swift
- // Naterade
- //
- // Inspired by http://martiancraft.com/blog/2015/03/core-data-stack/
- //
- import CoreData
- import os.log
- import HealthKit
- public protocol PersistenceControllerDelegate: AnyObject {
- /// Informs the delegate that a save operation will start, so it can start a background task on its behalf
- ///
- /// - Parameter controller: The persistence controller
- func persistenceControllerWillSave(_ controller: PersistenceController)
- /// Informs the delegate that a save operation did end
- ///
- /// - Parameters:
- /// - controller: The persistence controller
- /// - error: An error describing why the save failed
- func persistenceControllerDidSave(_ controller: PersistenceController, error: PersistenceController.PersistenceControllerError?)
- }
- /// Provides a Core Data persistence stack for the LoopKit data model
- public final class PersistenceController {
- public enum PersistenceControllerError: Error, LocalizedError {
- case configurationError(String)
- case coreDataError(NSError)
- public var errorDescription: String? {
- switch self {
- case .configurationError(let description):
- return description
- case .coreDataError(let error):
- return error.localizedDescription
- }
- }
- public var recoverySuggestion: String? {
- switch self {
- case .configurationError:
- return "Unrecoverable Error"
- case .coreDataError(let error):
- return error.localizedRecoverySuggestion
- }
- }
- }
- internal let managedObjectContext: NSManagedObjectContext
- public let isReadOnly: Bool
- public let directoryURL: URL
- public weak var delegate: PersistenceControllerDelegate?
- private let log = OSLog(category: "PersistenceController")
- private var queue = DispatchQueue(label: "com.loopkit.PersistenceController", qos: .utility)
- // MARK: - ReadyState
- private enum ReadyState {
- case waiting
- case ready
- case error(PersistenceControllerError)
- }
- public typealias ReadyCallback = (_ error: PersistenceControllerError?) -> Void
- private var readyCallbacks: [ReadyCallback] = []
- private var readyState: ReadyState = .waiting
- func onReady(_ callback: @escaping ReadyCallback) {
- queue.async {
- switch self.readyState {
- case .waiting:
- self.readyCallbacks.append(callback)
- case .ready:
- callback(nil)
- case .error(let error):
- callback(error)
- }
- }
- }
- /// Initializes a new persistence controller in the specified directory
- ///
- /// - Parameters:
- /// - directoryURL: The directory where the SQLlite database is stored. Will be created with no file protection if it doesn't exist.
- /// - model: The managed object model definition
- /// - isReadOnly: Whether the persistent store is intended to be read-only. Read-only stores will observe cross-process notifications and reload all contexts when data changes. Writable stores will post these notifications.
- public init(
- directoryURL: URL,
- isReadOnly: Bool = false
- ) {
-
- guard let url = LocalBundle.main.url(forResource: "Model", withExtension: "momd") else {
- log.error("Could not find Model url")
- fatalError("Unable to find Model url")
- }
-
- guard let model = NSManagedObjectModel(contentsOf: url) else {
- log.error("Could not open Model url at %@", String(describing: url))
- fatalError("Unable to find Model url")
- }
-
- managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
- managedObjectContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
- managedObjectContext.automaticallyMergesChangesFromParent = true
- self.directoryURL = directoryURL
- self.isReadOnly = isReadOnly
-
- initializeStack(inDirectory: directoryURL, model: model)
- }
- @discardableResult
- func save(_ completion: ((_ error: PersistenceControllerError?) -> Void)? = nil) -> PersistenceControllerError? {
- var error: PersistenceControllerError?
- self.managedObjectContext.performAndWait {
- guard self.managedObjectContext.hasChanges else {
- completion?(nil)
- return
- }
- error = self.saveInternal()
- completion?(error)
- }
-
- return error
- }
-
- // Should only be called from managedObjectContext thread
- internal func saveInternal() -> PersistenceControllerError? {
- guard !self.isReadOnly else {
- return nil
- }
- do {
- delegate?.persistenceControllerWillSave(self)
- try self.managedObjectContext.save()
- delegate?.persistenceControllerDidSave(self, error: nil)
- return nil
- } catch let saveError as NSError {
- self.log.error("Error while saving context: %{public}@", saveError)
- delegate?.persistenceControllerDidSave(self, error: .coreDataError(saveError))
- return .coreDataError(saveError)
- }
- }
- // Should only be called on managedObjectContext thread
- func updateMetadata(key: String, value: Any?) {
- if let coordinator = self.managedObjectContext.persistentStoreCoordinator, let store = coordinator.persistentStores.first {
- var metadata = coordinator.metadata(for: store)
- metadata[key] = value
- coordinator.setMetadata(metadata, for: store)
- }
- }
-
- // Should only be called on managedObjectContext thread
- func fetchMetadata(key: String) -> Any? {
- if let coordinator = self.managedObjectContext.persistentStoreCoordinator, let store = coordinator.persistentStores.first {
- let metadata = coordinator.metadata(for: store)
- return metadata[key]
- } else {
- return nil
- }
- }
-
- // MARK: -
- private func initializeStack(inDirectory directoryURL: URL, model: NSManagedObjectModel) {
- managedObjectContext.perform {
- var error: PersistenceControllerError?
- let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
- self.managedObjectContext.persistentStoreCoordinator = coordinator
- do {
- try FileManager.default.ensureDirectoryExists(at: directoryURL, with: FileProtectionType.completeUntilFirstUserAuthentication)
- } catch {
- // Ignore errors here, let Core Data explain the problem
- }
- let storeURL = directoryURL.appendingPathComponent("Model.sqlite")
- var options: [AnyHashable : Any] = [
- NSMigratePersistentStoresAutomaticallyOption: true,
- NSInferMappingModelAutomaticallyOption: true
- ]
-
- #if os(iOS)
- options[NSPersistentStoreFileProtectionKey] = FileProtectionType.completeUntilFirstUserAuthentication
- #endif
- do {
- try coordinator.addPersistentStore(ofType: NSSQLiteStoreType,
- configurationName: nil,
- at: storeURL,
- options: options
- )
- } catch let storeError as NSError {
- self.log.error("Failed to initialize persistenceController: %{public}@", storeError)
- error = .coreDataError(storeError)
- }
- self.queue.async {
- if let error = error {
- self.readyState = .error(error)
- } else {
- self.readyState = .ready
- }
- for callback in self.readyCallbacks {
- callback(error)
- }
- self.readyCallbacks = []
- }
- }
- }
- }
- extension PersistenceController: CustomDebugStringConvertible {
- public var debugDescription: String {
- return [
- "## PersistenceController",
- "* isReadOnly: \(isReadOnly)",
- "* directoryURL: \(directoryURL)",
- "* persistenceStoreCoordinator: \(String(describing: managedObjectContext.persistentStoreCoordinator))",
- ].joined(separator: "\n")
- }
- }
- // MARK: - Anchor store/fetch helpers
- extension PersistenceController {
- func storeAnchor(_ anchor: HKQueryAnchor?, key: String) {
- managedObjectContext.perform {
- let encoded: Data?
- if let anchor = anchor {
- encoded = try? NSKeyedArchiver.archivedData(withRootObject: anchor, requiringSecureCoding: true)
- if encoded == nil {
- self.log.error("Encoding anchor %{public} failed.", String(describing: anchor))
- }
- } else {
- encoded = nil
- }
- self.updateMetadata(key: key, value: encoded)
- let _ = self.saveInternal()
- }
- }
-
- func fetchAnchor(key: String, completion: @escaping (HKQueryAnchor?) -> Void) {
- managedObjectContext.perform {
- let value = self.fetchMetadata(key: key)
- if let encoded = value as? Data {
- let anchor = try? NSKeyedUnarchiver.unarchivedObject(ofClass: HKQueryAnchor.self, from: encoded)
- if anchor == nil {
- self.log.error("Decoding anchor from %{public}@ failed.", String(describing: encoded))
- }
- completion(anchor)
- } else {
- self.log.error("Anchor metadata invalid %{public}@.", String(describing: value))
- completion(nil)
- }
- }
- }
- }
- fileprivate extension FileManager {
-
- func ensureDirectoryExists(at url: URL, with protectionType: FileProtectionType? = nil) throws {
- try createDirectory(at: url, withIntermediateDirectories: true, attributes: protectionType.map { [FileAttributeKey.protectionKey: $0 ] })
- guard let protectionType = protectionType else {
- return
- }
- // double check protection type
- var attrs = try attributesOfItem(atPath: url.path)
- if attrs[FileAttributeKey.protectionKey] as? FileProtectionType != protectionType {
- attrs[FileAttributeKey.protectionKey] = protectionType
- try setAttributes(attrs, ofItemAtPath: url.path)
- }
- }
-
- }
|