4

I am using Core Data for the first time in a project made in XCode 8, swift 3. I have used background context (calling container.performBackgroundTask block..) to save the data and main context to fetch the data. When my app re-launches, the data I saved in the private background context is deleted.

Please tell me where I went wrong !!!

Here I call CoreDataManager class save context method in applicationDidEnterBackground and applicationWillTerminate methods of AppDelegate class:

class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
lazy var coreDataMgrInstance = CoreDataManager.sharedInstance

func applicationDidEnterBackground(_ application: UIApplication) {

    coreDataMgrInstance.saveContext()
}
func applicationWillTerminate(_ application: UIApplication) {
    coreDataMgrInstance.saveContext()
}}

Here is my Singleton class CoreDataManager to initiate NSpersistentContainer

class CoreDataManager: NSObject {    
class var sharedInstance: CoreDataManager {
    struct Singleton {
        static let instance = CoreDataManager()
    }

    return Singleton.instance
}
   private override init() {

    super.init()
}

 lazy var persistentContainer: NSPersistentContainer = {

   let container = NSPersistentContainer(name: "E_CareV2")

    let description = NSPersistentStoreDescription() // enable auto lightweight migratn
    description.shouldInferMappingModelAutomatically = true
    description.shouldMigrateStoreAutomatically = true

    container.persistentStoreDescriptions = [description]

    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    container.viewContext.automaticallyMergesChangesFromParent = true
    return container
}()
func saveContext(){

    print("saveContext")
    let context = persistentContainer.viewContext
    if context.hasChanges {
        do {
            try context.save()
        } catch {
            let nserror = error as NSError
            fatalError("Failure to save main context \(nserror), \(nserror.userInfo)")
        }
}}

Now this is the class where I save and fetch the data from Core Data

class SenderViewController: UIViewController {

var persistentContainer: NSPersistentContainer!

override func viewDidLoad() {
    super.viewDidLoad()

 persistentContainer = CoreDataManager.sharedInstance.persistentContainer
 let results = self.fetchPersistentData(entityName: "School", withPredicate: nil)
    print("results \n \(results)")       
  }

 @IBAction func enterPressed(_ sender: Any) {

  self.persistentContainer.performBackgroundTask({ (backgroundContext) in

        backgroundContext.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump

        let schoolentity = NSEntityDescription.insertNewObject(forEntityName: "School", into: backgroundContext) as! School

                schoolentity.schoolName = "ABC"
                schoolentity.schoolWebSite = "http://anywebsite.com/"

            do {
                try backgroundContext.save()
            } catch {
                fatalError("Failure to save background context: \(error)")
                }
    })
}

func fetchPersistentData(entityName: String?, withPredicate: NSPredicate?) -> [NSManagedObject]{

        let context = self.persistentContainer.viewContext
        let request: NSFetchRequest<School> = School.fetchRequest()
        let newentity = NSEntityDescription.entity(forEntityName: entityName!, in: context)

        request.entity = newentity
        request.returnsObjectsAsFaults = false

        do {
            let searchResults = try context.fetch(request) as [NSManagedObject]
            return searchResults               
        } catch {
            print("Error with request: \(error)")
        }
 return []
}
Nupur Sharma
  • 1,106
  • 13
  • 15
  • `return searchResults` has no effect in `viewDidLoad` and I doubt that the code even compiles. Not related but why do you cast up the specific type `[School]` returned from the fetch to more unspecific type `[NSManagedObject]`? – vadian Jun 24 '17 at 09:22
  • I just edited the code. The results I get is fine. But i don't get the same result when I re- run the app. – Nupur Sharma Jun 24 '17 at 09:36
  • Most likely it's.because you're relying on app lifecycle events like entering the background to save changes, instead of saving changes when you make changes. – Tom Harrington Jun 24 '17 at 22:53
  • I have saved the background context when I made changes in SenderViewControlller class – Nupur Sharma Jun 25 '17 at 08:22

3 Answers3

7

Actually the lightweight migrations are enabled by default as you can see on the screenshotenter image description here

So you can safely delete these lines:

let description = NSPersistentStoreDescription() // enable auto lightweight migratn
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true

container.persistentStoreDescriptions = [description]

After that everything should work.

Sergey Pekar
  • 8,555
  • 7
  • 47
  • 54
4

There are two ways to use NSPersistentContainer - the simple way and the correct way. You are mixing them which is leading to problems. Apple is a little inconsistent on this in its documentation so it is understandable that you are confused.

The Simple Way - The simple way is only use the viewContext both for reading and for writing and only from the main thread. There are never any conflicts because core-data is assessed via a single thread. The problem with this approach is that you can't run anything in a background thread. So if you are importing a lot of data the UI will freeze. If you have a super simple app (a small task list?) this will work OK, but not something that I would ever recommend for a serious app. It is OK for a testing app for a beginner to learn core-data.

The Correct Way - the correct way is to NEVER write to the viewContext EVER. Apple documents this in NSPersistentContainer documentation (but also in its template creates a save method for the viewContext?!). In this setup all writes to core data MUST go through performBackgroundTask and you have to call save on that context before the end of the block. You also need an operation queue to make sure that there are no merge conflicts see NSPersistentContainer concurrency for saving to core data. This setup is a lot harder to do correctly. Objects from performBackgroundTask contexts cannot leave the block and objects from the viewContext cannot be used in the block. The complier doesn't help you with this so it is something that you always need to watch out for.

Your problem is mergePolicy. mergePolicy is evil. If you have conflicts in core-data you have already done something wrong and any merge policy will lose data. In the simple setup there are no conflicts because it is all on one thread. In the correct setup there is no conflicts because of the queue you created when using performBackgroundTask. The problem is that if you use BOTH performBackgroundTask and write the the viewContext you can get conflicts and you will lose data. Personally, I think it is better to have no mergePolicy and crash then to silently lose data.

Jon Rose
  • 8,373
  • 1
  • 30
  • 36
  • Can you please tell me, what context should I use to save the data in applicationWillTerminate() method and how to do it? Removing merge policy did not work for me. – Nupur Sharma Jun 25 '17 at 08:33
  • You shouldn't have to save anything in `applicationWillTerminate`. Every change should happen in a performBackgroundTask that ends in a save. So you shouldn't have unsaved context floating around that you have to worry about when you exit. – Jon Rose Jun 25 '17 at 09:50
  • I deleted saveContext and mergepolicy, fetched data in main context and saved on performBackgroundTask block but Nothing Helped. I still cannot find the data i saved in previous run. – Nupur Sharma Jun 27 '17 at 04:33
  • Then I suspect that you are doing something else in the app that is deleting the data. – Jon Rose Jun 27 '17 at 06:51
  • I am saying that I looked at your code and I don't see a problem. So I suspect that there is other code that is deleting the database. – Jon Rose Jun 27 '17 at 07:18
  • This is the only place in the code where I have used core data. In other part of my code I have only played with the UI – Nupur Sharma Jun 27 '17 at 07:54
  • Check to see if persistentContainer is nil when doing the fetch – Jon Rose Jun 27 '17 at 07:57
  • The value of NSPersistentContainer is same (NSPersistentContainer: 0x608000045610) in case of save and fetch request. – Nupur Sharma Jun 27 '17 at 08:10
  • Then I am out of ideas. Sorry. – Jon Rose Jun 27 '17 at 08:13
  • "the correct way is to NEVER write to the viewContext EVER. Apple documents this in NSPersistentContainer documentation" I think this is a pretty strong statement, and I think you're misunderstanding the documentation. Could you add some more justification for it? The viewContext property is read-only, that just means you can't set a new view context, not that the context should not be written to. In 90% of apps multithreaded core data is not necessary. – jrturton Jun 29 '17 at 10:28
  • If contexts are writing at the same time core-data does not know what to do. The default behavior is to crash. You can set a merge policy to tell it what changes to ignore, but that is a bad solution because you still lose data. So you have two options - either only read and write to a single context on the main thread - which is OK if you have a simple app. Or make a queue to enqueue all your writes. This is what pros have been doing for some time (see this video - https://vimeo.com/89370886 about that setup). `NSPersistentContainer` uses a queue in `performBackgroundTask`. – Jon Rose Jun 29 '17 at 10:49
3

I figured out what causes my data not to be persistent in Core Data. It was these below 4 lines of code that I put in persistentContainer definition for enabling LIGHTWEIGHT MIGRATION of models:

let description = NSPersistentStoreDescription() // enable auto lightweight migratn
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true

container.persistentStoreDescriptions = [description]

When I deleted these lines, I became able to retain my data when the app re-launches. I had written the above lines to enable Lightweight Migration to my model, but I did not change my model or created new version of the model, making the Core Data unable to search the destination model in NSBundle and thus unable to infer the Mapping.

I am still not sure, how that would delete my data but I will keep trying to figure this out too and comment when I get success in it... :)

Nupur Sharma
  • 1,106
  • 13
  • 15