3

Still working on converting an app over from downloading information every time it uses or displays it, to caching it on-phone using CoreData (courtesy of MagicalRecord). This is on iOS 7

Because we don't have a data-push system set up to automatically update the phone's cached data whenever some data changes on the backend, I've been thinking over the last many months (as we worked on other aspects of the app) how to manage keeping a local copy of the data on the phone and being able to have the most up to date data in the cache.

I realized that as long as I still fetch the data every time :-( I can use the phone's CoreData backed cache of data to display and use, and just use the fetch of the data to update the on-phone database.

So I have been converting over the main data objects from being downloaded data making up a complete object, to these main data objects being light stand-in objects for CoreData objects.

Basically, each of the normal data objects in the app, instead of containing all the properties of the object internally, contains only the objectIDof the underlying CoreData object and maybe the app specific ID internally, and all other properties are dynamic and gotten from the CoreData object and passed through (most properties are read-only and updates are done through bulk-rewriting of the core data from passed in JSON)

Like this:

- (NSString *)amount
{
    __block NSString *result = nil;

    NSManagedObjectContext *localContext = [NSManagedObjectContext MR_newContext];

    [localContext performBlockAndWait:^{
        FinTransaction  *transaction = (FinTransaction *)[localContext existingObjectWithID:[self objectID] error:nil];

        if (nil != transaction)
        {
            result = [transaction.amount stringValue];
        }
    }];

    return result;
}

Occasionally there is one that needs to be set and those look like this:

- (void)setStatus:(MyTransactionStatus)status
{
    [MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
        FinTransaction *transaction = (FinTransaction *)[localContext existingObjectWithID:[self objectID] error:nil];

        if (nil != transaction)
        {
            transaction.statusValue = status;
        }

    } completion:^(BOOL success, NSError *error){}];
}

Now, my issue is that I have a view controller that basically uses an NSFetchedResultsController to display stored data from the local phone's CoreData database in a table view. At the same time as this is happening, and the user may start to scroll through the data, the phone spins off a thread to download updates to the data and then starts updating the CoreData data store with the updated data, at which point it then runs an asynchronous GCD call back on the main thread to have the fetched results controller refetch its data and and tells the table view to reload.

The problem is that if a user is scrolling through the initial fetched results controller fetched data and table view load, and the background thread is updating the same Core Data objects in the background, deadlocks occur. It is not the exact same entities being fetched and rewritten (when a deadlock occurs), i.e., not that object ID 1 is being read and written, but that the same persistent data store is being used.

Every access, read or write, happens in a MR_saveWithBlock or MR_saveWithBlockAndWait (writes/updates of data) as the case may be, and a [localContext performBlock:] or [localContext performBlockAndWait:] as may be appropriate. Each separate read or write has its own NSManagedObjectContext. I have not seen any where there are stray pending changes hanging around, and the actual places it blocks and deadlocks is not always the same, but always has to do with the main thread reading from the same persistent store as the background thread is using to update the data.

The fetched results controller is being created like this:

_frController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
                                                    managedObjectContext:[NSManagedObjectContext MR_rootSavingContext]
                                                      sectionNameKeyPath:sectionKeyPath
                                                               cacheName:nil];

and then an performFetch is done.

How can I best structure this sort of action where I need to display the extent data in a table view and update the data store in the background with new data?

While I am using MagicalRecord for most of it, I am open to comments, answers, etc with or without (straight CD) using MagicalRecord.

Tony Arnold
  • 2,738
  • 1
  • 23
  • 34
chadbag
  • 1,837
  • 2
  • 20
  • 34
  • Code edits actually make it harder to read imho, but that is ok. My code formatting style was developed based on research done in the late 80s that I read in a journal then, as amended and expanded by me through use since then and makes use of the way we humans read and process information vs the cramped and hard to read styles that were developed to deal with 80x24 or 80x25 character terminals that have been passed down through the ages... But I digress. :) – chadbag Jul 31 '14 at 15:40

1 Answers1

3

So the way I'd handle this is to look at having two managed object contexts each with its own persistent store coordinator. Both of the persistent store coordinators talk to the same persistent store on disk.

This approach is outlined in some detail in Session 211 from WWDC 2013 — "Core Data Performance Optimization and Debugging", which you can get to on Apple's Developer Site for WWDC 2013.

In order to use this approach with MagicalRecord, you will need to look at using the upcoming MagicalRecord 3.0 release, with the ClassicWithBackgroundCoordinatorSQLiteMagicalRecordStack (yes, that name needs work!). It implements the approach outlined in the WWDC session, although you need to be aware that there will be changes needed to your project to support MagicalRecord 3, and that it's also not quite released yet.

Essentially what you end up with is:

  • 1 x Main Thread Context: You use this to populate your UI, and for your fetched results controllers, etc. Don't ever make changes in this context.
  • 1 x Private Queue Context: Make all of your changes using the block-based saved methods — they automatically funnel through this context and save to disk.

I hope that makes sense — definitely watch the WWDC session — they use some great animated diagrams to explain why this approach is faster (and shouldn't block the main thread as much as the approach you're using now).

I'm happy to go into more detail if you need it.

Tony Arnold
  • 2,738
  • 1
  • 23
  • 34
  • Watched the video. Downloaded the current working MR 3. Working on this. Thanks! – chadbag Jul 31 '14 at 20:12
  • To use `ClassicWithBackgroundCoordinatorSQLiteMagicalRecordStack` I assume that the setup would be `setupClassicStackWithSQLiteStoreNamed:`? Is there a different ClassicStack than the `ClassicWithBackgroundCoordinatorSQLiteMagicalRecordStack`? – chadbag Jul 31 '14 at 20:23
  • `ClassicWithBackgroundCoordinatorSQLiteMagicalRecordStack` (oh god the name of that thing!) is a subclass of `ClassicSQLiteMagicalRecordStack`, so yes, something like `[ClassicWithBackgroundCoordinatorSQLiteMagicalRecordStack stackWithStoreNamed:@"Blah"];` will setup the stack for you. Keep in mind, **it's your responsibility to hold onto that stack in MR3**. There are other stack factory methods that accept a path, a URL and managed object models, so you should be able to find one to suit your needs. – Tony Arnold Jul 31 '14 at 22:22
  • Thanks. Dawned on me as I was driving home yesterday afternoon to do a better job exploring the header files and .m files to figure this out. Will have more time today to think clearly. Thanks! – chadbag Aug 01 '14 at 17:59
  • Do you have some sample code on how to access the main thread context and the private queue context? `[NSManagedObjectContext MR_mainQueueContext]` and `[NSManagedObjectContext MR_privateQueueContext]` return new contexts not connected to anything as far as I can tell by looking at the code. They don't know about my entities (`+entityForName: could not locate an NSManagedObjectModel for entity name` errors). When you say I ended up with 1 x Main and 1 x Private, is that conceptual and I really get a new one of a given type as needed? or is it really 2 MOC are created and used as parents? – chadbag Aug 01 '14 at 23:39
  • I think I have this working. Using the current MR3 dev stuff from github. The place I was having issues seems to be working, and all the other CD/MR backed pieces of the app are functional. Still need to do exhaustive testing. I create a `ClassicWithBackgroundCoordinatorSQLiteMagicalRecordStack` instance and store it away in a helper object that lives whenever the user is logged in and hence accessing data. I do `[myStackInstance context]` whenever I need to use the main queue for UI. I let the `[MagicalRecord saveWithBlock*]` stuff handle the "save" private context. Pass objID as needed. – chadbag Aug 04 '14 at 20:58
  • Perhaps my app has to work with some data from Internet in background context (private context 1), at the same time user must be able to change persisted data (private context 2). Are those private context must be linked with one Persistent Store Coordinator? Or each private context must have it's own Persistent Store Coordinator (3 pcs total)? – kas-kad Feb 01 '15 at 14:08
  • @TonyArnold pls check my comment ^ – kas-kad Feb 03 '15 at 08:31