PersistenceController.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. //
  2. // PersistenceController.swift
  3. // Naterade
  4. //
  5. // Inspired by http://martiancraft.com/blog/2015/03/core-data-stack/
  6. //
  7. import CoreData
  8. import os.log
  9. import HealthKit
  10. public protocol PersistenceControllerDelegate: AnyObject {
  11. /// Informs the delegate that a save operation will start, so it can start a background task on its behalf
  12. ///
  13. /// - Parameter controller: The persistence controller
  14. func persistenceControllerWillSave(_ controller: PersistenceController)
  15. /// Informs the delegate that a save operation did end
  16. ///
  17. /// - Parameters:
  18. /// - controller: The persistence controller
  19. /// - error: An error describing why the save failed
  20. func persistenceControllerDidSave(_ controller: PersistenceController, error: PersistenceController.PersistenceControllerError?)
  21. }
  22. /// Provides a Core Data persistence stack for the LoopKit data model
  23. public final class PersistenceController {
  24. public enum PersistenceControllerError: Error, LocalizedError {
  25. case configurationError(String)
  26. case coreDataError(NSError)
  27. public var errorDescription: String? {
  28. switch self {
  29. case .configurationError(let description):
  30. return description
  31. case .coreDataError(let error):
  32. return error.localizedDescription
  33. }
  34. }
  35. public var recoverySuggestion: String? {
  36. switch self {
  37. case .configurationError:
  38. return "Unrecoverable Error"
  39. case .coreDataError(let error):
  40. return error.localizedRecoverySuggestion
  41. }
  42. }
  43. }
  44. internal let managedObjectContext: NSManagedObjectContext
  45. public let isReadOnly: Bool
  46. public let directoryURL: URL
  47. public weak var delegate: PersistenceControllerDelegate?
  48. private let log = OSLog(category: "PersistenceController")
  49. private var queue = DispatchQueue(label: "com.loopkit.PersistenceController", qos: .utility)
  50. // MARK: - ReadyState
  51. private enum ReadyState {
  52. case waiting
  53. case ready
  54. case error(PersistenceControllerError)
  55. }
  56. public typealias ReadyCallback = (_ error: PersistenceControllerError?) -> Void
  57. private var readyCallbacks: [ReadyCallback] = []
  58. private var readyState: ReadyState = .waiting
  59. func onReady(_ callback: @escaping ReadyCallback) {
  60. queue.async {
  61. switch self.readyState {
  62. case .waiting:
  63. self.readyCallbacks.append(callback)
  64. case .ready:
  65. callback(nil)
  66. case .error(let error):
  67. callback(error)
  68. }
  69. }
  70. }
  71. /// Initializes a new persistence controller in the specified directory
  72. ///
  73. /// - Parameters:
  74. /// - directoryURL: The directory where the SQLlite database is stored. Will be created with no file protection if it doesn't exist.
  75. /// - model: The managed object model definition
  76. /// - 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.
  77. public init(
  78. directoryURL: URL,
  79. isReadOnly: Bool = false
  80. ) {
  81. guard let url = LocalBundle.main.url(forResource: "Model", withExtension: "momd") else {
  82. log.error("Could not find Model url")
  83. fatalError("Unable to find Model url")
  84. }
  85. guard let model = NSManagedObjectModel(contentsOf: url) else {
  86. log.error("Could not open Model url at %@", String(describing: url))
  87. fatalError("Unable to find Model url")
  88. }
  89. managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
  90. managedObjectContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
  91. managedObjectContext.automaticallyMergesChangesFromParent = true
  92. self.directoryURL = directoryURL
  93. self.isReadOnly = isReadOnly
  94. initializeStack(inDirectory: directoryURL, model: model)
  95. }
  96. @discardableResult
  97. func save(_ completion: ((_ error: PersistenceControllerError?) -> Void)? = nil) -> PersistenceControllerError? {
  98. var error: PersistenceControllerError?
  99. self.managedObjectContext.performAndWait {
  100. guard self.managedObjectContext.hasChanges else {
  101. completion?(nil)
  102. return
  103. }
  104. error = self.saveInternal()
  105. completion?(error)
  106. }
  107. return error
  108. }
  109. // Should only be called from managedObjectContext thread
  110. internal func saveInternal() -> PersistenceControllerError? {
  111. guard !self.isReadOnly else {
  112. return nil
  113. }
  114. do {
  115. delegate?.persistenceControllerWillSave(self)
  116. try self.managedObjectContext.save()
  117. delegate?.persistenceControllerDidSave(self, error: nil)
  118. return nil
  119. } catch let saveError as NSError {
  120. self.log.error("Error while saving context: %{public}@", saveError)
  121. delegate?.persistenceControllerDidSave(self, error: .coreDataError(saveError))
  122. return .coreDataError(saveError)
  123. }
  124. }
  125. // Should only be called on managedObjectContext thread
  126. func updateMetadata(key: String, value: Any?) {
  127. if let coordinator = self.managedObjectContext.persistentStoreCoordinator, let store = coordinator.persistentStores.first {
  128. var metadata = coordinator.metadata(for: store)
  129. metadata[key] = value
  130. coordinator.setMetadata(metadata, for: store)
  131. }
  132. }
  133. // Should only be called on managedObjectContext thread
  134. func fetchMetadata(key: String) -> Any? {
  135. if let coordinator = self.managedObjectContext.persistentStoreCoordinator, let store = coordinator.persistentStores.first {
  136. let metadata = coordinator.metadata(for: store)
  137. return metadata[key]
  138. } else {
  139. return nil
  140. }
  141. }
  142. // MARK: -
  143. private func initializeStack(inDirectory directoryURL: URL, model: NSManagedObjectModel) {
  144. managedObjectContext.perform {
  145. var error: PersistenceControllerError?
  146. let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
  147. self.managedObjectContext.persistentStoreCoordinator = coordinator
  148. do {
  149. try FileManager.default.ensureDirectoryExists(at: directoryURL, with: FileProtectionType.completeUntilFirstUserAuthentication)
  150. } catch {
  151. // Ignore errors here, let Core Data explain the problem
  152. }
  153. let storeURL = directoryURL.appendingPathComponent("Model.sqlite")
  154. var options: [AnyHashable : Any] = [
  155. NSMigratePersistentStoresAutomaticallyOption: true,
  156. NSInferMappingModelAutomaticallyOption: true
  157. ]
  158. #if os(iOS)
  159. options[NSPersistentStoreFileProtectionKey] = FileProtectionType.completeUntilFirstUserAuthentication
  160. #endif
  161. do {
  162. try coordinator.addPersistentStore(ofType: NSSQLiteStoreType,
  163. configurationName: nil,
  164. at: storeURL,
  165. options: options
  166. )
  167. } catch let storeError as NSError {
  168. self.log.error("Failed to initialize persistenceController: %{public}@", storeError)
  169. error = .coreDataError(storeError)
  170. }
  171. self.queue.async {
  172. if let error = error {
  173. self.readyState = .error(error)
  174. } else {
  175. self.readyState = .ready
  176. }
  177. for callback in self.readyCallbacks {
  178. callback(error)
  179. }
  180. self.readyCallbacks = []
  181. }
  182. }
  183. }
  184. }
  185. extension PersistenceController: CustomDebugStringConvertible {
  186. public var debugDescription: String {
  187. return [
  188. "## PersistenceController",
  189. "* isReadOnly: \(isReadOnly)",
  190. "* directoryURL: \(directoryURL)",
  191. "* persistenceStoreCoordinator: \(String(describing: managedObjectContext.persistentStoreCoordinator))",
  192. ].joined(separator: "\n")
  193. }
  194. }
  195. // MARK: - Anchor store/fetch helpers
  196. extension PersistenceController {
  197. func storeAnchor(_ anchor: HKQueryAnchor?, key: String) {
  198. managedObjectContext.perform {
  199. let encoded: Data?
  200. if let anchor = anchor {
  201. encoded = try? NSKeyedArchiver.archivedData(withRootObject: anchor, requiringSecureCoding: true)
  202. if encoded == nil {
  203. self.log.error("Encoding anchor %{public} failed.", String(describing: anchor))
  204. }
  205. } else {
  206. encoded = nil
  207. }
  208. self.updateMetadata(key: key, value: encoded)
  209. let _ = self.saveInternal()
  210. }
  211. }
  212. func fetchAnchor(key: String, completion: @escaping (HKQueryAnchor?) -> Void) {
  213. managedObjectContext.perform {
  214. let value = self.fetchMetadata(key: key)
  215. if let encoded = value as? Data {
  216. let anchor = try? NSKeyedUnarchiver.unarchivedObject(ofClass: HKQueryAnchor.self, from: encoded)
  217. if anchor == nil {
  218. self.log.error("Decoding anchor from %{public}@ failed.", String(describing: encoded))
  219. }
  220. completion(anchor)
  221. } else {
  222. self.log.error("Anchor metadata invalid %{public}@.", String(describing: value))
  223. completion(nil)
  224. }
  225. }
  226. }
  227. }
  228. fileprivate extension FileManager {
  229. func ensureDirectoryExists(at url: URL, with protectionType: FileProtectionType? = nil) throws {
  230. try createDirectory(at: url, withIntermediateDirectories: true, attributes: protectionType.map { [FileAttributeKey.protectionKey: $0 ] })
  231. guard let protectionType = protectionType else {
  232. return
  233. }
  234. // double check protection type
  235. var attrs = try attributesOfItem(atPath: url.path)
  236. if attrs[FileAttributeKey.protectionKey] as? FileProtectionType != protectionType {
  237. attrs[FileAttributeKey.protectionKey] = protectionType
  238. try setAttributes(attrs, ofItemAtPath: url.path)
  239. }
  240. }
  241. }