CoreDataStack.swift 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import CoreData
  2. import Foundation
  3. class CoreDataStack: ObservableObject {
  4. init() {}
  5. static let shared = CoreDataStack()
  6. static let identifier = "CoreDataStack"
  7. lazy var persistentContainer: NSPersistentContainer = {
  8. let container = NSPersistentContainer(name: "Core_Data")
  9. container.loadPersistentStores(completionHandler: { _, error in
  10. guard let error = error as NSError? else { return }
  11. fatalError("Unresolved error: \(error), \(error.userInfo)")
  12. })
  13. return container
  14. }()
  15. // ensure thread safety by creating a NSManagedObjectContext for the main thread and for a background thread
  16. lazy var backgroundContext: NSManagedObjectContext = {
  17. let newbackgroundContext = CoreDataStack.shared.persistentContainer.newBackgroundContext()
  18. newbackgroundContext.automaticallyMergesChangesFromParent = true
  19. newbackgroundContext
  20. .mergePolicy =
  21. NSMergeByPropertyStoreTrumpMergePolicy // if two objects with the same unique constraint are found, overwrite with the object in the external storage
  22. return newbackgroundContext
  23. }()
  24. lazy var viewContext: NSManagedObjectContext = {
  25. let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  26. viewContext.automaticallyMergesChangesFromParent = true
  27. return viewContext
  28. }()
  29. // MARK: - Fetch Requests
  30. //
  31. // the first I define here is for background work...I decided to pass a parameter context to the function to execute it on the viewContext if necessary, but for updating the UI I've decided to rather create a second generic fetch function with a completion handler which results are returned on the main thread
  32. //
  33. // first fetch function
  34. // fetch on the thread of the backgroundContext
  35. func fetchEntities<T: NSManagedObject>(
  36. ofType type: T.Type,
  37. predicate: NSPredicate,
  38. key: String,
  39. ascending: Bool,
  40. fetchLimit: Int? = nil,
  41. batchSize: Int? = nil,
  42. propertiesToFetch: [String]? = nil,
  43. context: NSManagedObjectContext? = CoreDataStack.shared.backgroundContext,
  44. callingFunction: String = #function,
  45. callingClass: String = #fileID
  46. ) -> [T] {
  47. let request = NSFetchRequest<T>(entityName: String(describing: type))
  48. request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)]
  49. request.predicate = predicate
  50. if let limit = fetchLimit {
  51. request.fetchLimit = limit
  52. }
  53. if let batchSize = batchSize {
  54. request.fetchBatchSize = batchSize
  55. }
  56. if let propertiesTofetch = propertiesToFetch {
  57. request.propertiesToFetch = propertiesTofetch
  58. request.resultType = .managedObjectResultType
  59. } else {
  60. request.resultType = .managedObjectResultType
  61. }
  62. var result: [T]?
  63. /// we need to ensure that the fetch immediately returns a value as long as the whole app does not use the async await pattern, otherwise we could perform this asynchronously with backgroundContext.perform and not block the thread
  64. context?.performAndWait {
  65. do {
  66. debugPrint(
  67. "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.succeeded) on Thread: \(Thread.current)"
  68. )
  69. result = try context?.fetch(request)
  70. } catch let error as NSError {
  71. debugPrint(
  72. "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.failed) \(error) on Thread: \(Thread.current)"
  73. )
  74. }
  75. }
  76. // if result == nil {
  77. // debugPrint("Fetch result is nil in \(callingFunction) from \(callingClass) on thread \(Thread.current)")
  78. // } else {
  79. // debugPrint(
  80. // "Fetch result count: \(result?.count ?? 0) in \(callingFunction) from \(callingClass) on thread \(Thread.current)"
  81. // )
  82. // }
  83. return result ?? []
  84. }
  85. // second fetch function
  86. // fetch and update UI
  87. func fetchEntitiesAndUpdateUI<T: NSManagedObject>(
  88. ofType type: T.Type,
  89. predicate: NSPredicate,
  90. key: String,
  91. ascending: Bool,
  92. fetchLimit: Int? = nil,
  93. batchSize: Int? = nil,
  94. propertiesToFetch: [String]? = nil,
  95. callingFunction: String = #function,
  96. callingClass: String = #fileID,
  97. completion: @escaping ([T]) -> Void
  98. ) {
  99. let request = NSFetchRequest<NSManagedObjectID>(entityName: String(describing: type))
  100. request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)]
  101. request.predicate = predicate
  102. request.resultType = .managedObjectIDResultType
  103. if let limit = fetchLimit {
  104. request.fetchLimit = limit
  105. }
  106. if let batchSize = batchSize {
  107. request.fetchBatchSize = batchSize
  108. }
  109. if let propertiesToFetch = propertiesToFetch {
  110. request.propertiesToFetch = propertiesToFetch
  111. }
  112. // perform fetch in the background
  113. //
  114. // the fetch returns a NSManagedObjectID which can be safely passed to the main queue because they are thread safe
  115. backgroundContext.perform {
  116. var result: [NSManagedObjectID]?
  117. do {
  118. debugPrint(
  119. "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.succeeded) on thread \(Thread.current)"
  120. )
  121. result = try self.backgroundContext.fetch(request)
  122. } catch let error as NSError {
  123. debugPrint(
  124. "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.failed) \(error) on thread \(Thread.current)"
  125. )
  126. }
  127. // change to the main queue to update UI
  128. DispatchQueue.main.async {
  129. if let result = result {
  130. debugPrint(
  131. "Returning fetch result to main thread in \(callingFunction) from \(callingClass) on thread \(Thread.current)"
  132. )
  133. // Convert NSManagedObjectIDs to objects in the main context
  134. let mainContext = self.viewContext
  135. let mainContextObjects = result.compactMap { mainContext.object(with: $0) as? T }
  136. completion(mainContextObjects)
  137. } else {
  138. debugPrint("Fetch result is nil in \(callingFunction) from \(callingClass) on thread \(Thread.current)")
  139. completion([])
  140. }
  141. }
  142. }
  143. }
  144. // fetch and only return a NSManagedObjectID
  145. func fetchNSManagedObjectID<T: NSManagedObject>(
  146. ofType type: T.Type,
  147. predicate: NSPredicate,
  148. key: String,
  149. ascending: Bool,
  150. fetchLimit: Int? = nil,
  151. batchSize: Int? = nil,
  152. propertiesToFetch: [String]? = nil,
  153. callingFunction: String = #function,
  154. callingClass: String = #fileID,
  155. completion: @escaping ([NSManagedObjectID]) -> Void
  156. ) {
  157. let request = NSFetchRequest<NSManagedObjectID>(entityName: String(describing: type))
  158. request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)]
  159. request.predicate = predicate
  160. request.resultType = .managedObjectIDResultType
  161. if let limit = fetchLimit {
  162. request.fetchLimit = limit
  163. }
  164. if let batchSize = batchSize {
  165. request.fetchBatchSize = batchSize
  166. }
  167. if let propertiesToFetch = propertiesToFetch {
  168. request.propertiesToFetch = propertiesToFetch
  169. }
  170. // Perform fetch in the background
  171. backgroundContext.perform {
  172. var result: [NSManagedObjectID]?
  173. do {
  174. debugPrint(
  175. "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.succeeded) on thread \(Thread.current)"
  176. )
  177. result = try self.backgroundContext.fetch(request)
  178. } catch let error as NSError {
  179. debugPrint(
  180. "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.failed) \(error) on thread \(Thread.current)"
  181. )
  182. }
  183. completion(result ?? [])
  184. }
  185. }
  186. // MARK: - Save
  187. //
  188. // takes a context as a parameter to be executed either on the main thread or on a background thread
  189. // save on the thread of the backgroundContext
  190. func saveContext(useViewContext: Bool = false, callingFunction: String = #function, callingClass: String = #fileID) throws {
  191. let contextToUse = useViewContext ? viewContext : backgroundContext
  192. try contextToUse.performAndWait {
  193. if contextToUse.hasChanges {
  194. do {
  195. try self.backgroundContext.save()
  196. debugPrint(
  197. "Saving to Core Data successful in \(callingFunction) in \(callingClass): \(DebuggingIdentifiers.succeeded)"
  198. )
  199. } catch let error as NSError {
  200. debugPrint(
  201. "Saving to Core Data failed in \(callingFunction) in \(callingClass): \(DebuggingIdentifiers.failed) with error \(error), \(error.userInfo)"
  202. )
  203. throw error
  204. }
  205. }
  206. }
  207. }
  208. }