magicalpanda / MagicalRecord

Super Awesome Easy Fetching for Core Data!
Other
10.8k stars 1.79k forks source link

Serving an NSFetchedResultsController tableView reload and updating underlying data in a separate thread #805

Open chadbag opened 10 years ago

chadbag commented 10 years ago

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).

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.

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), 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. 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.

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?

tonyarnold commented 10 years ago

Hi @chadbag — sorry it's taken a couple of weeks to reply to this. In future, you'll get a much quicker response posting best practice and approach questions like this on StackOverflow. If you haven't already, I'd post this over there as more eyes and minds will see and think about it there.

chadbag commented 10 years ago

I'll do that. I had considered that before posting here, but was not sure that this sort of thing was appropriate for SO as it seemed to me to be a more general type question and not a piece of specific code. It seems to me I had seen some sort of caution against general "best practice" type of things on SO, though, of course, I can explicitly find it on SO now.

thanks!

tonyarnold commented 10 years ago

I think SO likes to veer away from arguments about which approach is better, but asking for the right way to solve a problem is OK. It's honestly fine for you to post it here too, but as you've seen there are times where we don't get to the meatier questions quickly. If you post a link here to the SO thread you create, I'll do my best to have a look this weekend.

chadbag commented 10 years ago

HI. I basically posted the same thing there with some minor edits and additional pieces of info

http://stackoverflow.com/questions/25047019/nsfetchedresultscontroller-feeding-table-view-while-background-update-of-same-pe

tonyarnold commented 10 years ago

Thanks mate — having a read through now. One quick question here: What version of MagicalRecord are you using?

chadbag commented 10 years ago

Is there an easy way to tell? I updated from GitHub a couple months ago.

Sent from my iPhone

On Jul 31, 2014, at 7:17, Tony Arnold notifications@github.com wrote:

Thanks mate \ having a read through now. One quick question here: What version of MagicalRecord are you using?

\ Reply to this email directly or view it on GitHub.

tonyarnold commented 10 years ago

You should be able to call NSLog(@"MagicalRecord Version: %i", [MagicalRecord version]); in your app and it will print to the console.

chadbag commented 10 years ago

Cool thanks. I did not see that call. I'll try when I get to the office this morning. Should be able to just call it in the debugger.

Sent from my iPhone

On Jul 31, 2014, at 7:24, Tony Arnold notifications@github.com wrote:

You should be able to call NSLog(@"MagicalRecord Version: %i", [MagicalRecord version]); in your app and it will print to the console.

\ Reply to this email directly or view it on GitHub.

chadbag commented 10 years ago

The version is listed as 230

I read your response on SO. How does one get the 3.0? Is that release/3.0 branch?

Thanks!

tonyarnold commented 10 years ago

Hi Chad, yes - MagicalRecord 3.0 is currently in the release/3.0 branch.

chadbag commented 10 years ago

Thanks. I was not sure if you ran a separate 3.0 dev version with a "release" version. I got it installed and am working to get stuff working now. Thanks. I may have a some questions I cannot figure out but will work to get it all figured out first.

chadbag commented 10 years ago

Quick question. Using ClassicWithBackgroundCoordinatorSQLiteMagicalRecordStack and a [MagicalRecord saveWithBlockAndWait:...] where the save is on the background thread and a later read is on the main thread, I am not always seeing changes made on the background thread being merged into the main thread. Most of the time it seems to be doing it, but not always. I have logging turned on and sometimes see merge messages and sometimes not. Are there any specific MR3 things using this stack that I need to be aware of?