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 @@
CD3884532657BA550070FD6F /* _CoreNWSAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD3884332657BA410070FD6F /* _CoreNWSAlert.swift */; };
CD69DBC22666381500FD2A7C /* OneWeatherAnalytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD69DBC12666381500FD2A7C /* OneWeatherAnalytics.framework */; };
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 */; };
/* End PBXBuildFile section */
......@@ -86,6 +87,7 @@
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>"; };
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; };
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 */
......@@ -144,6 +146,7 @@
CD38841D2657BA400070FD6F /* CoreDataUtils.swift */,
CD3884062657BA190070FD6F /* CoreDataStorage.h */,
CD3884072657BA190070FD6F /* Info.plist */,
CE0E006B26C739680060CBB6 /* CoreDataStack.swift */,
);
path = CoreDataStorage;
sourceTree = "<group>";
......@@ -375,6 +378,7 @@
CD3884512657BA550070FD6F /* _CoreDailyWeather.swift in Sources */,
CD3884522657BA550070FD6F /* _CoreNotifications.swift in Sources */,
CD3884532657BA550070FD6F /* _CoreNWSAlert.swift in Sources */,
CE0E006C26C739680060CBB6 /* CoreDataStack.swift in Sources */,
CD38843F2657BA430070FD6F /* CoreDataStorage.swift in Sources */,
CD38843B2657BA430070FD6F /* CoreDataAppModelConvertable.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
import WidgetKit
public class CoreDataStorage: Storage {
private let modelName = "1WModel"
private var lastSavedAppData: AppData? = nil
private let log = Logger(componentName: "CoreDataStorage 💾")
private lazy var managedContext: NSManagedObjectContext? = {
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
}()
private let stack = CoreDataStack()
public init() {}
public func save(locations: [Location], selectedIndex: Int?) {
log.info("Save: start")
managedContext?.perform { [weak self] in
guard let self = self else { return }
let appData = AppData(selectedIndex: selectedIndex, locations: locations)
guard appData != self.lastSavedAppData else {
self.log.info("Save: no changes, skip")
return
}
guard let context = self.managedContext else {
return
}
do {
try self.deleteAll(in: context)
if let coreAppData = try CoreAppData(context: context, appModel: appData) {
context.insert(coreAppData)
try self.save(context: context)
self.lastSavedAppData = appData
// This shouldn't be here in theory, but it's the simplest way to work around the DelayedSaveStorage.
// TODO: find a better place for it.
if #available(iOS 14, *) {
WidgetCenter.shared.reloadAllTimelines()
stack.getManagedObjectContext { [weak self] context in
context?.perform {
guard let self = self else { return }
let appData = AppData(selectedIndex: selectedIndex, locations: locations)
guard appData != self.lastSavedAppData else {
self.log.info("Save: no changes, skip")
return
}
guard let context = context else {
return
}
do {
try self.deleteAll(in: context)
if let coreAppData = try CoreAppData(context: context, appModel: appData) {
context.insert(coreAppData)
try self.save(context: context)
self.lastSavedAppData = appData
// This shouldn't be here in theory, but it's the simplest way to work around the DelayedSaveStorage.
// TODO: find a better place for it.
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) {
log.info("Load: start")
managedContext?.perform { [weak self] in
guard let self = self else { return }
guard let context = self.managedContext else {
return
}
let completionWithErrorHandling: StorageCompletion = { [weak self] (locations, selectedIndex, error) in
if error != nil {
self?.log.error("Load: error.")
stack.getManagedObjectContext { [weak self] context in
context?.perform {
guard let self = self else { return }
guard let context = context else {
return
}
else {
self?.log.info("Load: success. \(locations?.count ?? 0) locations, selected: \( selectedIndex ?? -1)")
let completionWithErrorHandling: StorageCompletion = { [weak self] (locations, selectedIndex, error) in
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()
fetchRequest.fetchLimit = 1
let results = try context.fetch(fetchRequest)
guard let coreAppData = results.first else {
completionWithErrorHandling([], nil, nil)
return
do {
let fetchRequest: NSFetchRequest<CoreAppData> = CoreAppData.fetchRequest()
fetchRequest.fetchLimit = 1
let results = try context.fetch(fetchRequest)
guard let coreAppData = results.first else {
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