CoreDataStack.swift 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  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<T>(entityName: String(describing: type))
  100. request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)]
  101. request.predicate = predicate
  102. if let limit = fetchLimit {
  103. request.fetchLimit = limit
  104. }
  105. if let batchSize = batchSize {
  106. request.fetchBatchSize = batchSize
  107. }
  108. if let propertiesToFetch = propertiesToFetch {
  109. request.propertiesToFetch = propertiesToFetch
  110. request.resultType = .managedObjectResultType
  111. } else {
  112. request.resultType = .managedObjectResultType
  113. }
  114. backgroundContext.perform {
  115. var result: [T]?
  116. do {
  117. debugPrint(
  118. "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.succeeded) on thread \(Thread.current)"
  119. )
  120. result = try self.backgroundContext.fetch(request)
  121. } catch let error as NSError {
  122. debugPrint(
  123. "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.failed) \(error) on thread \(Thread.current)"
  124. )
  125. }
  126. // Ensure that the fetch immediately returns a value
  127. DispatchQueue.main.async {
  128. if let result = result {
  129. debugPrint(
  130. "Returning fetch result to main thread in \(callingFunction) from \(callingClass) on thread \(Thread.current)"
  131. )
  132. completion(result)
  133. } else {
  134. debugPrint("Fetch result is nil in \(callingFunction) from \(callingClass) on thread \(Thread.current)")
  135. completion([])
  136. }
  137. }
  138. }
  139. }
  140. // MARK: - Save
  141. //
  142. // takes a context as a parameter to be executed either on the main thread or on a background thread
  143. // save on the thread of the backgroundContext
  144. func saveContext(useViewContext: Bool = false, callingFunction: String = #function, callingClass: String = #fileID) throws {
  145. let contextToUse = useViewContext ? viewContext : backgroundContext
  146. try contextToUse.performAndWait {
  147. if contextToUse.hasChanges {
  148. do {
  149. try self.backgroundContext.save()
  150. debugPrint(
  151. "Saving to Core Data successful in \(callingFunction) in \(callingClass): \(DebuggingIdentifiers.succeeded)"
  152. )
  153. } catch let error as NSError {
  154. debugPrint(
  155. "Saving to Core Data failed in \(callingFunction) in \(callingClass): \(DebuggingIdentifiers.failed) with error \(error), \(error.userInfo)"
  156. )
  157. throw error
  158. }
  159. }
  160. }
  161. }
  162. }