Knowing when lightweight migration finished - swift

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.

Related

Creating more than one entity on SwiftUI

I am trying to get more than one entity for my coding project at school but I have an error saying invalid redeclaration of data controller.
class DataController: ObservableObject{
let container = NSPersistentContainer(name: "Blood Sugar")
init() {
container.loadPersistentStores { description, error in
if let error = error {
print("Core Data failed to load: \(error.localizedDescription)")
}
}
}
}
class DataController : ObservableObject{
let containers = NSPersistentContainer(name: "Carbohydrates")
init(){
containers.loadPersistentStores{ description, errors in
if let errors = errors{
print("Core data failed to load: \(errors.localizedDescription)")
}
}
}
}
Since you tagged this as SwiftUI, DataController should be a struct. We use value types like structs now to solve a lot of the bugs caused by using objects in UIKit and ObjC. You can see Apple's doc Choosing Between Structures and Classes for more info.
If you use an Xcode app template project and check "Use core data" you'll see a PersistenceController struct that will demonstrate how to do it correctly. I've included it below:
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for _ in 0..<10 {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
}
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "SearchTest")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
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)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
}
}
You can create more entities in the model editor. There is usually only one NSPersistentContainer per app. The container can have multiple stores (usually sqlite databases). Then you can assign different entities to each store too. To create an instance of an entity you do that on a NSManagedObjectContext and you can choose which store to save it too, although most of the time people use one store which is the default.

How to change the concurrency type for Core Data in Swift

When you create a SwiftUI core data app, nowadays, Xcode creates this struct.
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
return result
}()
var context:NSManagedObjectContext {
return container.viewContext
}
var container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "PharmaTrac")
container.newBackgroundContext()
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: {(storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
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)")
}
})
}
}
I want to change the context concurrency type to private queue, like in this
var managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
But this way apple creates it, the context is created automatically.
How do I create a context with .privateQueueConcurrencyType?
That code creates an NSPersistentContainer, then gets the viewContext from the container. The container can also create other contexts, if you want. You can either call container.newBackgroundContext() to create a new context like the one you describe (it will use private queue concurrency). You can also use context.performBackgroundTask(_:) to have the container create a new private-queue context on the fly for some background work.

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.

Core data: Failed to load model

I am new to core data.
What I am trying to DO: I am trying to create a cocoatouch framework that has an app to add employee details and display them in a table view. So that i can add this framework to my main project to work independently.
Issues I face: The frame work builds without any error. I have added the core data stack from swift 3 to the framework. But when i run the main project, the moment the framework loads the log displays "Failed to load model named Simple framework", "fetch failed" and "employee must have a valid entity description". The code that I have used in the framework is as shown below :
public class CoreDataStack {
public static let sharedInstance = CoreDataStack()
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "SimpleFramework")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error {
fatalError("Unresolved error \(error), \(error)")
}
})
return container
}()
public func saveContext() {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch let error as NSError {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
}
}
#IBAction func addEmployee(_ sender: Any) {
//To save the data
let context = CoreDataStack.sharedInstance.persistentContainer.viewContext
let employee = Employee(context: context)
employee.employeeName = nameTextField.text
employee.employeeAge = Int16(ageTextField.text!)!
employee.hasVehicle = hasVehicle.isOn
CoreDataStack.sharedInstance.saveContext()
navigationController!.popViewController(animated: true)
}
#IBAction func addEmployee(_ sender: Any) {
//To save the data
let context = CoreDataStack.sharedInstance.persistentContainer.viewContext
let employee = Employee(context: context)
employee.employeeName = nameTextField.text
employee.employeeAge = Int16(ageTextField.text!)!
employee.hasVehicle = hasVehicle.isOn
CoreDataStack.sharedInstance.saveContext()
navigationController!.popViewController(animated: true)
}
I've had this issue, when I had wrong model name - it should be models name, not the projects (see the screen shot)
Explicitly pass the models file name to the Core Data stack for initialization and make sure, it is loaded from the right bundle at the time (test bundle, app bundle...) by using Bundle(for: type(of: self)):
//...
let momdName = "SimpleFramework" //pass this as a parameter
//...
guard let modelURL = Bundle(for: type(of: self)).url(forResource: momdName, withExtension:"momd") else {
fatalError("Error loading model from bundle")
}
guard let mom = NSManagedObjectModel(contentsOf: modelURL) else {
fatalError("Error initializing mom from: \(modelURL)")
}
persistentContainer = NSPersistentContainer(name: momdName, managedObjectModel: mom)
//...
Edit:
Also make sure, the SimpleFramework.xcdatamodeld is added to the used targets Target Membership:
The string you pass to the NSPersistentContainer initializer:
NSPersistentContainer(name: "CoreData")
needs to match the filename of the data model file in your Xcode project:
CoreData.xcdatamodeld
If you want to use CoreData in your dynamic framework you have to subclass NSPersistentContainer and use it instead of NSPersistentContainer.
class PersistentContainer: NSPersistentContainer { }
//...
lazy var container: PersistentContainer = {
let result = PersistentContainer(name: "Your xcdatamodeld file name here")
result.loadPersistentStores { (storeDescription, error) in
if let error = error {
print(error.localizedDescription)
}
}
return result
}()
In my case, for some reason the DataModel.xcdatamodeld became missing from my project workspace.
First I tried creating a new DataModle.xcdatamodeld and recreating the data model, but the same error occurred. Thats when I realized that the Original DataModel.xcdatamodeld was still in the root directory. I fixed this by simply right clicking my project in my project navigator, and selecting "Add files to "Project"...", then I added my old data model and deleted my new data model. Finally I hard cleaned, ran my project and it fixed the issue.
My problem was at my .podspec file. You should include the xcdatamodeld extension on the pod that you are creating.
s.resources = "myprojectfolder/**/*.{png,jpeg,jpg,storyboard,xib,xcassets,xcdatamodeld}"

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.