Isuru
Isuru

Reputation: 31283

Error creating a separate NSManagedObjectContext

Before getting into my issue, please have a look at this image.

enter image description here

Here is the actual data model: data model

I retrieve a set of Records from a web API, create objects out of them, save them in core data and display them in the Today view. By default these records are returned for the current date.

The user can tap on Past button to go to a separate view where he can choose a past or future date from a date picker view and view Records for that selected date. This means I have to call the API again passing the selected date, retrieve the data and save that data in core data and display them. When the user leaves this view, this data should be discarded.

This is the important part. Even though I get a new set of data, the old original data for the current date in the Today view must not go away. So if/when the user returns to the Today view, that data should be readily available as he left it without the app having to call the API and get the data for the current date again.

I thought of creating a separate NSManagedObjectContext to hold these temporary data.

I have a separate class called DatabaseManager to handle core data related tasks. This class initializes with an instance of `NSManagedObjectContext. It creates the managed object classes in the given context.

import CoreData
import Foundation
import MagicalRecord
import SwiftyJSON

public class DatabaseManager {

    private let context: NSManagedObjectContext!

    init(context: NSManagedObjectContext) {
        self.context = context
    }

    public func insertRecords(data: AnyObject, success: () -> Void, failure: (error: NSError?) -> Void) {
        let json = JSON(data)
        if let records = json.array {
            for recordObj in records {
                let record = Record.MR_createInContext(context) as Record
                record.id = recordObj["Id"].int
                record.name = recordObj["Name"].string!
                record.date = NSDate(string: recordObj["Date"].string!)
            }
            context.MR_saveToPersistentStoreAndWait()
            success()
        }
    }
}

So in the Today view I pass NSManagedObjectContext.MR_defaultContext() to insertRecords() method. I also have a method to fetch Records from the given context.

func fetchRecords(context: NSManagedObjectContext) -> [Record]? {
    return Record.MR_findAllSortedBy("name", ascending: true, inContext: context) as? [Record]
}

The data is retrieved from the API, saved in core data and gets displayed successfully. All good so far.

In the Past View, I have to do basically the same thing. But since I don't want the original data to change. I tried to do this a few ways which MagicalRecord provides.

Attempt #1 - NSManagedObjectContext.MR_context()

I create a new context with NSManagedObjectContext.MR_context(). I change the date in Past view, the data for that selected date gets retrieved and saved in the database successfully. But here's the issue. When I fetch the objects from core data, I get that old data as well. For example, each day has only 10 records. In Today view I display 10 records. When the fetch objects in the Past view, I get 20 objects! I assume it's the old 10 objects plus the new ones. Also when I try to display them in the tableview, it crashes with a EXC_BAD_ACCESS error in the cellForRowAtIndexPath method.

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell

    let record = records[indexPath.row]
    cell.textLabel?.text = record.name // EXC_BAD_ACCESS
    cell.detailTextLabel?.text = record.date.toString()

    return cell
}

Attempt #2 - NSManagedObjectContext.MR_newMainQueueContext()

The app crashes when I change the date with the following error.

'+entityForName: nil is not a legal NSPersistentStoreCoordinator for searching for entity name 'Record''

Attempt #3 - NSManagedObjectContext.MR_contextWithParent(NSManagedObjectContext.MR_defaultContext())

Same result as Attempt #1.

Attempt #4 - From Hal's Answer I learned that even though I create two MOCs, they both refer to the same NSPersistentStore. So I created another new store to hold the temporary data in my AppDelegate.

MagicalRecord.setupCoreDataStackWithStoreNamed("Records")
MagicalRecord.setupCoreDataStackWithStoreNamed("Records-Temp")

Then when I change the date to get the new data, I set that temporary store as the default store like this.

func getDate(date: NSDate) {
    let url = NSPersistentStore.MR_urlForStoreName("Records-Temp")
    let store = NSPersistentStore(persistentStoreCoordinator: NSPersistentStoreCoordinator.MR_defaultStoreCoordinator(), configurationName: nil, URL: url, options: nil)
    NSPersistentStore.MR_setDefaultPersistentStore(store)

    let context = NSManagedObjectContext.MR_defaultContext()
    viewModel.populateDatabase(date, context: context)
}

Note that I'm using the default context. I get the data but it's the same result as Attempt 1 and 3. I get 20 records. They include data from both the old date and the new date. If I use NSManagedObjectContext.MR_context(), it would simply crash like in Attempt 1.

I also discovered something else. After creating the stores in App Delegate, I printed out the default store name println(MagicalRecord.defaultStoreName()) in the Today's view. Strangely it didn't print the name I gave the store which is Records. Instead it showed Reports.sqlite. Reports being the project's name. Weird.

Why do I get the old data as well? Am I doing something with when initializing a new context?

Sorry if my question is a little confusing so I uploaded a demo project to my Dropbox. Hopefully that will help.

Any help is appreciated.

Thank you.

Upvotes: 1

Views: 601

Answers (2)

Daniel Galasko
Daniel Galasko

Reputation: 24237

Thread Safety

First of all I want to mention the Golden Rule of Core Data. NSManagedObject's are not thread safe, hence, "Thou shalt not cross the streams" (WWDC). What this means is that you should always access a Managed Object in its context and never pass it outside of its context. This is why your importer class worries me, you are inserting a bunch of objects into a context without guaranteeing that you are running the insert inside the Context.

One simple code change would fix this:

public func insertRecords(data: AnyObject, success: () -> Void, failure: (error: NSError?) -> Void) {
    let json = JSON(data)
    context.performBlock { () -> Void in
        //now we are thread safe :)
        if let records = json.array {
            for recordObj in records {
                let record = Record.MR_createInContext(context) as Record
                record.id = recordObj["Id"].int
                record.name = recordObj["Name"].string!
                record.date = NSDate(string: recordObj["Date"].string!)
            }
            context.MR_saveToPersistentStoreAndWait()
            success()
        }
    }
}

The only time you don't need to worry about this is when you are using the Main Queue Context and accessing objects on the main thread, like in tableview's etc.

Don't forget that MagicalRecord also has convenient save utilities that create context's ripe for saving :

MagicalRecord.saveWithBlock { (context) -> Void in
  //save me baby
}

Displaying Old Records

Now to your problem, the following paragraph in your post concerns me:

The user can tap on Past button to go to a separate view where he can choose a past or future date from a date picker view and view Records for that selected date. This means I have to call the API again passing the selected date, retrieve the data and save that data in core data and display them. When the user leaves this view, this data should be discarded.

I don't like the idea that you are discarding the information the user has requested once they leave that view. As a user I would expect to be able to navigate back to the old list and see the results I just queried without another unecessary network request. It might make more sense to maybe have a deletion utility that prunes your old objects on startup rather than while the user is accessing them.

Anyways, I cannot illustrate how important it is that you familiarize yourself with NSFetchedResultsController

This class is intended to efficiently manage the results returned from a Core Data fetch request.

You configure an instance of this class using a fetch request that specifies the entity, optionally a filter predicate, and an array containing at least one sort ordering. When you execute the fetch, the instance efficiently collects information about the results without the need to bring all the result objects into memory at the same time. As you access the results, objects are automatically faulted into memory in batches to match likely access patterns, and objects from previous accessed disposed of. This behavior further serves to keep memory requirements low, so even if you traverse a collection containing tens of thousands of objects, you should never have more than tens of them in memory at the same time.

Taken from Apple

It literally does everything for you and should be your go-to for any list that shows objects from Core Data.

When I fetch the objects from core data, I get that old data as well

Thats to be expected, you haven't specified anywhere that your fetch should include the reports in a certain date range. Here's a sample fetch:

let fetch = Record.MR_createFetchRequest()
let maxDateForThisController = NSDate()//get your date
fetch.predicate = NSPredicate(format: "date < %@", argumentArray: [maxDateForThisController])
fetch.fetchBatchSize = 10// or an arbitrary number
let dateSortDescriptor = NSSortDescriptor(key: "date", ascending: false)
let nameSortDescriptor = NSSortDescriptor(key: "name", ascending: true)

fetch.sortDescriptors = [dateSortDescriptor,nameSortDescriptor]//the order in which they are placed in the array matters

let controller = NSFetchedResultsController(fetchRequest: fetch,
        managedObjectContext: NSManagedObjectContext.MR_defaultContext(),
        sectionNameKeyPath: nil, cacheName: nil)

Importing Discardable Records

Finally, you say that you want to see old reports and use a separate context that won't save to the persistent store. Thats also simple, your importer takes a context so all you would need to do is make sure that your importer can support imports without saving to the persistent store. That way you can discard the context and the objects will go with it. So your method signature could look like this:

public func insertRecords(data: AnyObject, canSaveToPersistentStore: Bool = true,success: () -> Void, failure: (error: NSError?) -> Void) {

    /**
      Import some stuff
    */
    if canSaveToPersistentStore {
        context.MR_saveToPersistentStoreWithCompletion({ (complete, error) -> Void in
            if complete {
                success()
            } else {
                error
            }
        })
    } else {
        success()
    }
}

Upvotes: 2

Hal Mueller
Hal Mueller

Reputation: 7646

The old data that was in your persistent store, and addressed with the original MOC, is still there, and will be retrieved when the second MOC does a fetch. They're both looking at the same persistent store. It's just that the second MOC also has new data fetched from your API.

A synchronous network operation saving to Core Data will hang your app, and (for a large enough set of records) cause the system to kill your app, appearing to the user as a crash. Your client is wrong on that point, and needs to be educated.

Break apart your logic for fetching, saving, and viewing. Your view that shows a particular date's records should just do that--which it can do, if it accepts a date and uses a predicate.

Your 'cellForRowAtIndexPath' crash smells like a problem with a missing or misspelled identifier. What happens if you hard code a string instead of using 'record.name'?

Upvotes: 0

Related Questions