Core Data database wiped when using NSPersistentCloudKitContainer - swift

I currently have an application that uses Core Data with Apple's NSPersistentCloudKitContainer and I've a 2 users mentioning they lost their data when updating the app - I'm using Lightweight migrations, and the only factor they have in common is: both had no iCloud Storage left.
After further inspection I've noticed that if I go to Settings > iCloud > Disable it for my app, whenever I open my app again all my data will be gone.
As anyone run into this issue? Is this expected? Any way around it?
For reference, here's my setup code:
self.container = NSPersistentCloudKitContainer(name: "DATABASE_NAME")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
Crashlytics.crashlytics().record(error: error)
}
self.container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
self.container.viewContext.automaticallyMergesChangesFromParent = true
})

You could add a check for availability of iCloud container:
func isICloudContainerAvailable()-> Bool {
if let _ = FileManager.default.ubiquityIdentityToken {
return true
} else {
return false
}
}
Depending on results of the check you could return NSPersistentCloudKitContainer or NSPersistentContainer. You should also turn on NSPersistentHistoryTrackingKey for NSPersistentContainer case:
lazy var persistentContainer: NSPersistentContainer = {
var container: NSPersistentContainer!
if isICloudContainerAvailable() {
container = NSPersistentCloudKitContainer(name: "DATABASE_NAME")
} else {
container = NSPersistentContainer(name: "DATABASE_NAME")
let description = container.persistentStoreDescriptions.first
description?.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
}
...
return container
}
This way if you go to Settings > iCloud > Disable it for the app, data still should be available in the app (it will use NSPersistentContainer). If you enable iCloud back, all changes in the app will also sync.
But in case user manually delete iCloud data for the app in Manage Account Storage, data in the app will be lost, as it said in the alert: "This will delete all data for this app stored on this iPhone and in iCloud". I can't find the way to save local data for such case.

Related

Knowing when lightweight migration finished

Setup:
I'm trying to do a lightweight Core Data migration in my app. I added a new Core Data model version and added the new improvements (added 9 attributes to existing entities, 2 new entities, relationships between the new and existing entities).
Issue:
Switching from a build with the old database to the new one causes the app to have no data. But once I quit the app and come back, the data is all there (I'm assuming it was just being migrated).
Question:
Is there a way to know when the Core Data is starting a migration (to let the users know) and when the migration is finished (to refresh their view with the new data)? I've done so much searching this past couple of weeks and have not come with much.
Thank you in advance!
Code:
This is how I set up Core Data (specifically the setupContainer() code):
final class CoreDataManager {
static let sharedManager = CoreDataManager()
lazy var persistentContainer: NSPersistentContainer = {
setupContainer()
}()
private func setupContainer() -> NSPersistentContainer {
useCloudSync = UserDefaults.standard.bool(forKey: UserDefaults.Keys.useCloudSync)
let containerToUse: NSPersistentContainer?
if useCloudSync {
containerToUse = NSPersistentCloudKitContainer(name: "appName")
} else {
containerToUse = NSPersistentContainer(name: "appName")
}
//check if we have a container
guard let container = containerToUse else {
fatalError("Hey Listen! Could not get a container!!")
}
// Enable history tracking and remote notifications
guard let description = container.persistentStoreDescriptions.first else {
fatalError("Hey Listen! ###\(#function): Failed to retrieve a persistent store description.")
}
//be notified of change
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
description.setOption(true as NSNumber, forKey: NSMigratePersistentStoresAutomaticallyOption)
description.setOption(true as NSNumber, forKey: NSInferMappingModelAutomaticallyOption)
//set tracking history if we're using local container
if !useCloudSync {
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Hey Listen! Couldn't load persistent store. Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.transactionAuthor = appTransactionAuthorName
container.viewContext.automaticallyMergesChangesFromParent = true
//Remote changes notification
NotificationCenter.default.addObserver(
self, selector: #selector(type(of: self).storeRemoteChange(_:)),
name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator)
return container
}//end of setup container
}
Presumably you're calling loadPersistentStores on your NSPersistentContainer. That method accepts a completion handler (closure). That's where you'll get the callback that the migration has finished (or has failed). And you can tell when the migration is about to begin by adding a call just before loadPersistentStores.
If you create your own NSPersistentStoreDescription instance and add it to the NSPersistentContainer, you can control whether the migration happens and whether it runs synchronously or asynchronously.
See Enabling core data lightweight migration in Swift 3 (particularly answer https://stackoverflow.com/a/53607127/719690) for an example of using the completion handler.
Does the completionHandler of loadPersistentStores of NSPersistentContainer run synchronously? is also useful to understanding the flow.
Working from your code sample, you might revise it to (and I've switch to shortened closure syntax):
container.loadPersistentStores { storeDescription, error in
if let error = error as NSError? {
// error path, but should be nicer than just crashing
fatalError("Hey Listen! Couldn't load persistent store. Unresolved error \(error), \(error.userInfo)")
} else {
// happy path
print("unicorns and rainbows, migration succeeded, load the tableview")
}
}
(typed in browser, beware typos)
A Swiftier version would use a guard let on the error checking, but I wanted to minimize changes I made to your sample.

Core Data CloudKit Sync Problem on MacCatalyst

I have the strangest problem. I’ve developed a SwiftUI app for iPhone, iPad and MacCatalyst using Core Data and CloudKit private database to sync the user’s data across all his/her devices.
The problem is that when I make an update on an iOS device (iPhone or iPad), the update syncs across all iOS devices, but not to the Macs. Similarly, updates I make on the Macs, sync across the Macs, but not to the iOS devices.
If I delete the app on the Mac and its associated sqllite database in the app’s associated ~/Library/Container/<myapp_container>/Data subfolder and reinstall the app, only the «Mac data» gets refilled from CloudKit. Likewise if I delete the app on an iOS device, only the «iOS data» arrives from CloudKit. In other words, it behaves as if the MacCatalyst data and the iOS data are stored separately in CloudKit.
Now, if I compile the app onto the Mac (rather than installing an Archive app), the sync from the iOS devices go through. But only once. Subsequent updates are not synced until I compile the app onto the Mac again. Likewise, updates made on the Mac only get synced (once) to the iOS devices when I compile the app on the Mac. Recompiling to the iOS devices does not make any difference.
The PersistentCloudkitContainer class:
public class PersistentCloudKitContainer {
// MARK: - Define Constants / Variables
public static var context: NSManagedObjectContext {
return persistentContainer.viewContext
}
// MARK: - Initializer
private init() {}
// MARK: - Core Data stack
public static var persistentContainer: NSPersistentContainer = {
let container = NSPersistentCloudKitContainer(name: "MyModel")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
guard let description = container.persistentStoreDescriptions.first else {
fatalError("### PersistentCloudKitContainer->\(#function): Failed to retrieve persistant store description")
}
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.cloudKitContainerOptions?.databaseScope = .private
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return container
}()
// MARK: - Core Data Saving support
public static func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
The persistent container is called by the SwiftUI App struct:
struct MyApp: App {
let context = PersistentCloudKitContainer.persistentContainer.viewContext
var body: some Scene {
WindowGroup {
ContentView().environment(\.managedObjectContext, context)
}
}
}

Updated core data model without new version, how to fix?

I was working on an app update and I added and deleted some attributes and entities to my data model without creating a new version of it. It was my first app and unfortunately I was not using any version control system neither.
I have tried to delete the old sqlite file and recreate a new one when user updated the app but it crashes immediately after launch because one of the new attributes returns nil which I added recently.
I have a sign up screen so I thought I could show that screen again when app updated and fill my model with new data but that doesnt work either. I am compeletely lost as a beginner.
Scene Delegate
if !UserDefaults.standard.bool(forKey: K.DefaultKey.firstLaunch) {
UserDefaults.standard.set(true, forKey: K.DefaultKey.firstLaunch)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyboard.instantiateViewController(withIdentifier: "onboard")
self.window?.rootViewController = viewController
self.window?.makeKeyAndVisible()
}
My code to delete and rebuild
class CoreData {
static let datamodelName = "DataModel"
static let storeType = "sqlite"
static let persistentContainer = NSPersistentContainer(name: datamodelName)
private static let url: URL = {
let url = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0].appendingPathComponent("\(datamodelName).\(storeType)")
assert(FileManager.default.fileExists(atPath: url.path))
return url
}()
static func deleteAndRebuild() {
try! persistentContainer.persistentStoreCoordinator.destroyPersistentStore(at: url, ofType: storeType, options: nil)
persistentContainer.loadPersistentStores(completionHandler: { (nsPersistentStoreDescription, error) in
guard let error = error else {
return
}
fatalError(error.localizedDescription)
})
}
}
Appdelegate
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "DataModel")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
CoreDataContext.deleteAndRebuild()
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
Edit: I found hash codes of the old model via momd file. Can I use these hash codes to recreate old data model?
If possible, the best way to fix this would be to undo the model changes, so that it would be possible to load the current data. Then make the changes again properly so that migration could happen. You'd do something like
Start using source control.
Undo the recent changes (I know you don't have version control but you probably know what they were).
Verify step 1 by running the app using only that version of the model and making sure it loads data without crashing.
Create a new model version, make it the current version, and make your changes there. Core Data will then try to migrate the data to the new model.
[Update] The reason you're still getting a crash with your delete-and-rebuild code is that you're still calling fatalError after deleting and rebuilding the persistent store. If you've successfully recovered, you shouldn't also call fatalError, since its whole purpose is to crash the app. I don't know what you mean about one of the new attributes returning nil. That's expected of a new attribute, because since it's new, there's no data for it.

Toggle iCloud sync during runtime

I have a SwiftUI App, where the user can buy with in-app purchases some premium features. One of this features is iCloud sync over more devices. I am using CoreData to save users data.
My persistent container:
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "store name")
let description: NSPersistentStoreDescription? = container.persistentStoreDescriptions.first
let remoteChangeKey: String = "NSPersistentStoreRemoteChangeNotificationOptionKey"
if(description != nil) {
description!.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description!.setOption(true as NSNumber, forKey: remoteChangeKey)
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
My question is how can I toggle on/off cloud sync when the user buy a subscription. I don't want hat the user have to restart the app.
I also want that the user can toggle this setting in the in-app settings.
thanks
Declare your variable as a NSPersistentContainer instead of NSPersistentCloudKitContainer. On launch, if the user has cloud sync, load the cloud kit persistent container, otherwise load the non-cloud kit one.
When the switch is toggled, reload the container, following the same rules. To reload the container, I would add the property to a manager object, in which I would add some methods that reload the the container depending on the user's settings.

Core Data with pre-filled .sqlite (Swift3)

currently I'm working on a Swift3 / iOS10 update of an existing iOS9 App which stores about 10.000 charging points for electric vehicles across europe. Up to now I always shipped the Application with a pre-filled database (.sqlite, .sqlite-shm, .sqlite-wal files from the .xcappdata bundle), but with the current Version Apple is introducing the NSPersistentContainer Class, which makes it a bit more complicated. In my AppDelegate Class I'm instantiating my NSPersistentContainer object and passing it to a lazy var like it's done by Apple in every example code:
lazy var stationDataPersistentContainer: NSPersistentContainer = {
let fileMgr = FileManager.default
let destinationModel = NSPersistentContainer.defaultDirectoryURL()
if !fileMgr.fileExists(atPath: destinationModel.appendingPathComponent("StationData.sqlite").path) {
do {
try fileMgr.copyItem(at: URL(fileURLWithPath: Bundle.main.resourcePath!.appending("/StationData.sqlite")), to: destinationModel.appendingPathComponent("/StationData.sqlite"))
try fileMgr.copyItem(at: URL(fileURLWithPath: Bundle.main.resourcePath!.appending("/StationData.sqlite-shm")), to: destinationModel.appendingPathComponent("/StationData.sqlite-shm"))
try fileMgr.copyItem(at: URL(fileURLWithPath: Bundle.main.resourcePath!.appending("/StationData.sqlite-wal")), to: destinationModel.appendingPathComponent("/StationData.sqlite-wal"))
} catch {
//
}
} else {
//
}
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentContainer(name: "StationData")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
/*
* Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
* Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
In the iOS9 version im copying the files to the apropriate directory, like you can see in the following code example:
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("StationData.sqlite")
let fileMgr = NSFileManager.defaultManager()
if !fileMgr.fileExistsAtPath(url.path!) {
do {
try fileMgr.copyItemAtPath(NSBundle.mainBundle().pathForResource("StationData", ofType: "sqlite")!, toPath: self.applicationDocumentsDirectory.URLByAppment("StationData.sqlite").path!)
try fileMgr.copyItemAtPath(NSBundle.mainBundle().pathForResource("StationData", ofType: "sqlite-shm")!, toPath: self.applicationDocumentsDirectory.URLByAppendingPathComponent("StationData.sqlite-shm").path!)
try fileMgr.copyItemAtPath(NSBundle.mainBundle().pathForResource("StationData", ofType: "sqlite-wal")!, toPath: self.applicationDocumentsDirectory.URLByAppendingPathComponent("StationData.sqlite-wal").path!)
} catch {
//
} do {
try coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url,
options: [NSMigratePersistentStoresAutomaticallyOption:true, NSInferMappingModelAutomaticallyOption:true])
} catch {
//
}
} else {
//
}
return coordinator
}()
For a number of days I have tried to move the files to the proper directory which is returned by NSPersistentContainer.defaultDirectoryURL() -> URL, but everytime I get an error, that the file already exists because my stationDataPersistentContainer is already initialized and so the NSPersistentContainer had enough time to generate the sqlite* files. Even if I try to copy the files and initialize the stationDataPersistentContainer in an overwritten init() function I could not get this right. Is there anything I'm missing or overlooking in the documentation? Which is the best/right/appropriate way to copy existing data on installation of an App into coredata.
Appendix:
Just for your Information, I could also store the JSON-Files, which I get from my API into the Documents directory and run the JSON-parser, but this needs a lot of ressources and especially time! (This question is also posted on the Apple Developers Forum and waiting for approval)
This is how I do it:
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "app_name")
let seededData: String = "app_name"
var persistentStoreDescriptions: NSPersistentStoreDescription
let storeUrl = self.applicationDocumentsDirectory.appendingPathComponent("app_name.sqlite")
if !FileManager.default.fileExists(atPath: (storeUrl.path)) {
let seededDataUrl = Bundle.main.url(forResource: seededData, withExtension: "sqlite")
try! FileManager.default.copyItem(at: seededDataUrl!, to: storeUrl)
}
print(storeUrl)
container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: storeUrl)]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error {
fatalError("Unresolved error \(error),")
}
})
return container
}()
The simplest solution is to not use NSPersistentContainer - it's only a convenience for taking away the stack boilerplate, and if you're updating an already working app, you could just leave your code as it is.
If you want to migrate to NSPersistentContainer, then move the files before you have called loadPersistentStores - it's at that point that the SQL files are first created.
Note that NSPersistentContainer may not be using the documents directory - out of the box on the iOS simulator it uses the application support directory. If using the documents directory is important to you, then you have to subclass NSPersistentContainer and override the defaultDirectoryURL class method, or provide an NSPersistentStoreDescription to the NSPersistentContainer which tells it where to store the data.