Commit 4d6f4b51 by Demid Merzlyakov

IOS-142: CoreDataStorage waits for the persistent stores to load before using…

IOS-142: CoreDataStorage waits for the persistent stores to load before using the ManagedObjectContext.
parent 4f552b82
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
CD3884532657BA550070FD6F /* _CoreNWSAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD3884332657BA410070FD6F /* _CoreNWSAlert.swift */; }; CD3884532657BA550070FD6F /* _CoreNWSAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD3884332657BA410070FD6F /* _CoreNWSAlert.swift */; };
CD69DBC22666381500FD2A7C /* OneWeatherAnalytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD69DBC12666381500FD2A7C /* OneWeatherAnalytics.framework */; }; CD69DBC22666381500FD2A7C /* OneWeatherAnalytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD69DBC12666381500FD2A7C /* OneWeatherAnalytics.framework */; };
CD8EA914265D42E2000D3D63 /* 1WModel.xcdatamodeld in Resources */ = {isa = PBXBuildFile; fileRef = CD3884372657BA420070FD6F /* 1WModel.xcdatamodeld */; }; CD8EA914265D42E2000D3D63 /* 1WModel.xcdatamodeld in Resources */ = {isa = PBXBuildFile; fileRef = CD3884372657BA420070FD6F /* 1WModel.xcdatamodeld */; };
CE0E006C26C739680060CBB6 /* CoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0E006B26C739680060CBB6 /* CoreDataStack.swift */; };
CEEF40F9265E2EE600425D8F /* OneWeatherCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEEF40F8265E2EE600425D8F /* OneWeatherCore.framework */; }; CEEF40F9265E2EE600425D8F /* OneWeatherCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEEF40F8265E2EE600425D8F /* OneWeatherCore.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
...@@ -86,6 +87,7 @@ ...@@ -86,6 +87,7 @@
CD3884392657BA420070FD6F /* regenerate_objects.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = regenerate_objects.sh; sourceTree = "<group>"; }; CD3884392657BA420070FD6F /* regenerate_objects.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = regenerate_objects.sh; sourceTree = "<group>"; };
CD38843A2657BA420070FD6F /* CoreDataStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStorage.swift; sourceTree = "<group>"; }; CD38843A2657BA420070FD6F /* CoreDataStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStorage.swift; sourceTree = "<group>"; };
CD69DBC12666381500FD2A7C /* OneWeatherAnalytics.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OneWeatherAnalytics.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CD69DBC12666381500FD2A7C /* OneWeatherAnalytics.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OneWeatherAnalytics.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CE0E006B26C739680060CBB6 /* CoreDataStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStack.swift; sourceTree = "<group>"; };
CEEF40F8265E2EE600425D8F /* OneWeatherCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OneWeatherCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CEEF40F8265E2EE600425D8F /* OneWeatherCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OneWeatherCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E5F0E97C8CB8930C9E20B7FD /* Pods-CoreDataStorage.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoreDataStorage.debug.xcconfig"; path = "Target Support Files/Pods-CoreDataStorage/Pods-CoreDataStorage.debug.xcconfig"; sourceTree = "<group>"; }; E5F0E97C8CB8930C9E20B7FD /* Pods-CoreDataStorage.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoreDataStorage.debug.xcconfig"; path = "Target Support Files/Pods-CoreDataStorage/Pods-CoreDataStorage.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
...@@ -144,6 +146,7 @@ ...@@ -144,6 +146,7 @@
CD38841D2657BA400070FD6F /* CoreDataUtils.swift */, CD38841D2657BA400070FD6F /* CoreDataUtils.swift */,
CD3884062657BA190070FD6F /* CoreDataStorage.h */, CD3884062657BA190070FD6F /* CoreDataStorage.h */,
CD3884072657BA190070FD6F /* Info.plist */, CD3884072657BA190070FD6F /* Info.plist */,
CE0E006B26C739680060CBB6 /* CoreDataStack.swift */,
); );
path = CoreDataStorage; path = CoreDataStorage;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -375,6 +378,7 @@ ...@@ -375,6 +378,7 @@
CD3884512657BA550070FD6F /* _CoreDailyWeather.swift in Sources */, CD3884512657BA550070FD6F /* _CoreDailyWeather.swift in Sources */,
CD3884522657BA550070FD6F /* _CoreNotifications.swift in Sources */, CD3884522657BA550070FD6F /* _CoreNotifications.swift in Sources */,
CD3884532657BA550070FD6F /* _CoreNWSAlert.swift in Sources */, CD3884532657BA550070FD6F /* _CoreNWSAlert.swift in Sources */,
CE0E006C26C739680060CBB6 /* CoreDataStack.swift in Sources */,
CD38843F2657BA430070FD6F /* CoreDataStorage.swift in Sources */, CD38843F2657BA430070FD6F /* CoreDataStorage.swift in Sources */,
CD38843B2657BA430070FD6F /* CoreDataAppModelConvertable.swift in Sources */, CD38843B2657BA430070FD6F /* CoreDataAppModelConvertable.swift in Sources */,
CD38843D2657BA430070FD6F /* CoreDataError.swift in Sources */, CD38843D2657BA430070FD6F /* CoreDataError.swift in Sources */,
......
//
// CoreDataStack.swift
// CoreDataStorage
//
// Created by Demid Merzlyakov on 14.08.2021.
//
import CoreData
import OneWeatherAnalytics
class CoreDataStack {
private let log = Logger(componentName: "CoreDataStorage 💾")
private let modelName = "1WModel"
private lazy var persistentContainer: NSPersistentContainer = {
var container: NSPersistentContainer
if
let modelURL = Bundle(for: CoreDataStorage.self).url(forResource: "1WModel", withExtension: "momd"),
let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL),
let storeUrl = self.storeUrl{
container = NSPersistentContainer(name: "1WModel", managedObjectModel: managedObjectModel)
container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: storeUrl)]
}
else {
container = NSPersistentContainer(name: "1WModel")
}
return container
}()
private var managedContext: NSManagedObjectContext?
/// We need to wait for persistant stores to load before returning a managed object context to the Storage. That's what this is for.
/// A queue will not do, becase we can't use a queue to wait for a completion of an operation within Apple's code.
let initializationSemaphore = DispatchSemaphore(value: 1)
private var storeUrl: URL? {
let appGroupIdentifier = "group.com.onelouder.oneweather"
let fileManager = FileManager.default
guard let containerUrl = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else {
log.error("Failed to get container URL for app group \(appGroupIdentifier)!")
assertionFailure("Failed to get container URL for app group \(appGroupIdentifier)!")
return nil
}
return containerUrl.appendingPathComponent("\(modelName).sqlite")
}
/// The Persistent Store used to be in the app directory, but was then moved to an app group container to be shared with widgets.
/// For all users who still have it in the old location, we need to move it to the new one.
private func movePersistentStoreIfNeeded() {
let legacyStoreUrl = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("\(modelName).sqlite")
guard let storeUrl = self.storeUrl else {
return
}
let fileManager = FileManager.default
guard fileManager.fileExists(atPath: legacyStoreUrl.path) && !fileManager.fileExists(atPath: storeUrl.path) else {
return
}
do {
log.info("Moving the persistent store to the app group location.")
try fileManager.moveItem(at: legacyStoreUrl, to: storeUrl)
}
catch {
log.error("Error moving the model from the old location to the new one: \(error)")
}
}
public func getManagedObjectContext(_ completion: @escaping (NSManagedObjectContext?) -> ()) {
if let existingContext = self.managedContext {
completion(existingContext)
return
}
else {
initializationSemaphore.wait()
defer {
initializationSemaphore.signal()
}
if self.managedContext == nil {
self.managedContext = self.persistentContainer.newBackgroundContext()
}
completion(self.managedContext)
}
}
public init() {
initializationSemaphore.wait()
DispatchQueue.global(qos: .userInitiated).async {
self.movePersistentStoreIfNeeded()
self.persistentContainer.loadPersistentStores { [weak self] (description, error) in
defer {
self?.initializationSemaphore.signal()
}
if let error = error {
self?.log.error("Error loading persistent stores: \(error)")
}
}
}
}
}
...@@ -12,132 +12,81 @@ import OneWeatherAnalytics ...@@ -12,132 +12,81 @@ import OneWeatherAnalytics
import WidgetKit import WidgetKit
public class CoreDataStorage: Storage { public class CoreDataStorage: Storage {
private let modelName = "1WModel"
private var lastSavedAppData: AppData? = nil private var lastSavedAppData: AppData? = nil
private let log = Logger(componentName: "CoreDataStorage 💾") private let log = Logger(componentName: "CoreDataStorage 💾")
private lazy var managedContext: NSManagedObjectContext? = { private let stack = CoreDataStack()
persistentContainer.newBackgroundContext()
}()
/// The Persistent Store used to be in the app directory, but was then moved to an app group container to be shared with widgets.
/// For all users who still have it in the old location, we need to move it to the new one.
private func movePersistentStoreIfNeeded() {
let legacyStoreUrl = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("\(modelName).sqlite")
guard let storeUrl = self.storeUrl else {
return
}
let fileManager = FileManager.default
guard fileManager.fileExists(atPath: legacyStoreUrl.path) && !fileManager.fileExists(atPath: storeUrl.path) else {
return
}
do {
log.info("Moving the persistent store to the app group location.")
try fileManager.moveItem(at: legacyStoreUrl, to: storeUrl)
}
catch {
log.error("Error moving the model from the old location to the new one: \(error)")
}
}
private var storeUrl: URL? {
let appGroupIdentifier = "group.com.onelouder.oneweather"
let fileManager = FileManager.default
guard let containerUrl = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else {
log.error("Failed to get container URL for app group \(appGroupIdentifier)!")
assertionFailure("Failed to get container URL for app group \(appGroupIdentifier)!")
return nil
}
return containerUrl.appendingPathComponent("\(modelName).sqlite")
}
private lazy var persistentContainer: NSPersistentContainer = {
guard
let modelURL = Bundle(for: CoreDataStorage.self).url(forResource: "1WModel", withExtension: "momd"),
let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL),
let storeUrl = self.storeUrl
else {
return NSPersistentContainer(name: "1WModel")
}
movePersistentStoreIfNeeded()
let container = NSPersistentContainer(name: "1WModel", managedObjectModel: managedObjectModel)
container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: storeUrl)]
NSPersistentContainer.defaultDirectoryURL()
container.loadPersistentStores { [weak self] (description, error) in
if let error = error {
self?.log.error("Error loading persistent stores: \(error)")
}
}
return container
}()
public init() {} public init() {}
public func save(locations: [Location], selectedIndex: Int?) { public func save(locations: [Location], selectedIndex: Int?) {
log.info("Save: start") log.info("Save: start")
managedContext?.perform { [weak self] in stack.getManagedObjectContext { [weak self] context in
guard let self = self else { return } context?.perform {
let appData = AppData(selectedIndex: selectedIndex, locations: locations) guard let self = self else { return }
guard appData != self.lastSavedAppData else { let appData = AppData(selectedIndex: selectedIndex, locations: locations)
self.log.info("Save: no changes, skip") guard appData != self.lastSavedAppData else {
return self.log.info("Save: no changes, skip")
} return
guard let context = self.managedContext else { }
return guard let context = context else {
} return
do { }
try self.deleteAll(in: context) do {
if let coreAppData = try CoreAppData(context: context, appModel: appData) { try self.deleteAll(in: context)
context.insert(coreAppData) if let coreAppData = try CoreAppData(context: context, appModel: appData) {
try self.save(context: context) context.insert(coreAppData)
self.lastSavedAppData = appData try self.save(context: context)
// This shouldn't be here in theory, but it's the simplest way to work around the DelayedSaveStorage. self.lastSavedAppData = appData
// TODO: find a better place for it. // This shouldn't be here in theory, but it's the simplest way to work around the DelayedSaveStorage.
if #available(iOS 14, *) { // TODO: find a better place for it.
WidgetCenter.shared.reloadAllTimelines() if #available(iOS 14, *) {
WidgetCenter.shared.reloadAllTimelines()
}
} }
self.log.info("Save: success")
}
catch {
self.log.error("Save: error: \(error)")
} }
self.log.info("Save: success")
}
catch {
self.log.error("Save: error: \(error)")
} }
} }
} }
public func load(completion: @escaping StorageCompletion) { public func load(completion: @escaping StorageCompletion) {
log.info("Load: start") log.info("Load: start")
managedContext?.perform { [weak self] in stack.getManagedObjectContext { [weak self] context in
guard let self = self else { return } context?.perform {
guard let context = self.managedContext else { guard let self = self else { return }
return guard let context = context else {
} return
let completionWithErrorHandling: StorageCompletion = { [weak self] (locations, selectedIndex, error) in
if error != nil {
self?.log.error("Load: error.")
} }
else { let completionWithErrorHandling: StorageCompletion = { [weak self] (locations, selectedIndex, error) in
self?.log.info("Load: success. \(locations?.count ?? 0) locations, selected: \( selectedIndex ?? -1)") if error != nil {
self?.log.error("Load: error.")
}
else {
self?.log.info("Load: success. \(locations?.count ?? 0) locations, selected: \( selectedIndex ?? -1)")
}
completion(locations, selectedIndex, error)
} }
completion(locations, selectedIndex, error) do {
} let fetchRequest: NSFetchRequest<CoreAppData> = CoreAppData.fetchRequest()
do { fetchRequest.fetchLimit = 1
let fetchRequest: NSFetchRequest<CoreAppData> = CoreAppData.fetchRequest() let results = try context.fetch(fetchRequest)
fetchRequest.fetchLimit = 1 guard let coreAppData = results.first else {
let results = try context.fetch(fetchRequest) completionWithErrorHandling([], nil, nil)
guard let coreAppData = results.first else { return
completionWithErrorHandling([], nil, nil) }
return let appData: AppData = try coreAppData.toAppModel()
self.lastSavedAppData = appData
completionWithErrorHandling(appData.locations, appData.selectedIndex, nil)
}
catch {
self.log.error("Error during load: \(error)")
completionWithErrorHandling(nil, nil, error)
} }
let appData: AppData = try coreAppData.toAppModel()
self.lastSavedAppData = appData
completionWithErrorHandling(appData.locations, appData.selectedIndex, nil)
}
catch {
self.log.error("Error during load: \(error)")
completionWithErrorHandling(nil, nil, error)
} }
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment