Source code for the WriteFreely SwiftUI app for iOS, iPadOS, and macOS
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

139 lines
5.4 KiB

  1. import CoreData
  2. #if os(iOS)
  3. import UIKit
  4. #elseif os(macOS)
  5. import AppKit
  6. #endif
  7. final class LocalStorageManager {
  8. private let logger = Logging(for: String(describing: LocalStorageManager.self))
  9. public static var standard = LocalStorageManager()
  10. public let container: NSPersistentContainer
  11. private let containerName = "LocalStorageModel"
  12. private init() {
  13. container = NSPersistentContainer(name: containerName)
  14. setupStore(in: container)
  15. registerObservers()
  16. }
  17. func saveContext() {
  18. if container.viewContext.hasChanges {
  19. do {
  20. logger.log("Saving context to local store started...")
  21. try container.viewContext.save()
  22. logger.log("Context saved to local store.")
  23. } catch {
  24. logger.logCrashAndSetFlag(error: LocalStoreError.couldNotSaveContext)
  25. }
  26. }
  27. }
  28. func purgeUserCollections() throws {
  29. let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "WFACollection")
  30. let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
  31. do {
  32. logger.log("Purging user collections from local store...")
  33. try container.viewContext.executeAndMergeChanges(using: deleteRequest)
  34. logger.log("User collections purged from local store.")
  35. } catch {
  36. logger.log("\(LocalStoreError.couldNotPurgeCollections.localizedDescription)", level: .error)
  37. throw LocalStoreError.couldNotPurgeCollections
  38. }
  39. }
  40. }
  41. private extension LocalStorageManager {
  42. var oldStoreURL: URL {
  43. let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
  44. return appSupport.appendingPathComponent("LocalStorageModel.sqlite")
  45. }
  46. var sharedStoreURL: URL {
  47. let id = "group.com.abunchtell.writefreely"
  48. let groupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: id)!
  49. return groupContainer.appendingPathComponent("LocalStorageModel.sqlite")
  50. }
  51. func setupStore(in container: NSPersistentContainer) {
  52. if !FileManager.default.fileExists(atPath: oldStoreURL.path) {
  53. container.persistentStoreDescriptions.first!.url = sharedStoreURL
  54. }
  55. container.loadPersistentStores { _, error in
  56. self.logger.log("Loading local store...")
  57. if let error = error {
  58. self.logger.logCrashAndSetFlag(error: LocalStoreError.couldNotLoadStore(error.localizedDescription))
  59. }
  60. self.logger.log("Loaded local store.")
  61. }
  62. migrateStore(for: container)
  63. container.viewContext.automaticallyMergesChangesFromParent = true
  64. container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
  65. }
  66. func migrateStore(for container: NSPersistentContainer) {
  67. // Check if the shared store exists before attempting a migration — for example, in case we've already attempted
  68. // and successfully completed a migration, but the deletion of the old store failed for some reason.
  69. guard !FileManager.default.fileExists(atPath: sharedStoreURL.path) else { return }
  70. let coordinator = container.persistentStoreCoordinator
  71. // Get a reference to the old store.
  72. guard let oldStore = coordinator.persistentStore(for: oldStoreURL) else {
  73. return
  74. }
  75. // Attempt to migrate the old store over to the shared store URL.
  76. do {
  77. self.logger.log("Migrating local store to shared store...")
  78. try coordinator.migratePersistentStore(oldStore,
  79. to: sharedStoreURL,
  80. options: nil,
  81. withType: NSSQLiteStoreType)
  82. self.logger.log("Migrated local store to shared store.")
  83. } catch {
  84. logger.logCrashAndSetFlag(error: LocalStoreError.couldNotMigrateStore(error.localizedDescription))
  85. }
  86. // Attempt to delete the old store.
  87. do {
  88. logger.log("Deleting migrated local store...")
  89. try FileManager.default.removeItem(at: oldStoreURL)
  90. logger.log("Deleted migrated local store.")
  91. } catch {
  92. logger.logCrashAndSetFlag(
  93. error: LocalStoreError.couldNotDeleteStoreAfterMigration(error.localizedDescription)
  94. )
  95. }
  96. }
  97. func registerObservers() {
  98. let center = NotificationCenter.default
  99. #if os(iOS)
  100. let notification = UIApplication.willResignActiveNotification
  101. #elseif os(macOS)
  102. let notification = NSApplication.willResignActiveNotification
  103. #endif
  104. // We don't need to worry about removing this observer because we're targeting iOS 9+ / macOS 10.11+; the
  105. // system will clean this up the next time it would be posted to.
  106. // See: https://developer.apple.com/documentation/foundation/notificationcenter/1413994-removeobserver
  107. // And: https://developer.apple.com/documentation/foundation/notificationcenter/1407263-removeobserver
  108. // swiftlint:disable:next discarded_notification_center_observer
  109. center.addObserver(forName: notification, object: nil, queue: nil, using: self.saveContextOnResignActive)
  110. }
  111. func saveContextOnResignActive(_ notification: Notification) {
  112. saveContext()
  113. }
  114. }