0

I have a tableview in my app that contains a NSFetchedResultsController to load in some CoreData objects.

As the table builds in cellForRowAtIndexPath:, for each cell I must do a fetch to get some other info from another object.

The table is filled with UserTasks, and I must get some info from a UserSite (UserTask contains a siteID attribute)

I am getting the UserSite info in a background thread, and using a temporary context. It works fine, but it still wants to lag the UI a bit when scrolling.

        Site *site = [_scannedSites objectForKey:task.siteID];
        if(!site)
        {   
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{

                AppDelegate *ad = [AppDelegate sharedAppDelegate];
                NSManagedObjectContext *temporaryContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
                temporaryContext.persistentStoreCoordinator = ad.persistentStoreCoordinator;

                Site *site2 = [task getSiteWithContext:temporaryContext];
                if(site2)
                {
                    [ad.managedObjectContext performBlock:^{

                        Site *mainContextObject = (Site *)[ad.managedObjectContext objectWithID:site2.objectID];
                        [_scannedSites mainContextObject forKey:task.siteID];
                    }];
                    dispatch_async(dispatch_get_main_queue(), ^{
                        Site *newSite = [_scannedSites objectForKey:task.siteID];
                        cell.lblCustName.text = newSite.siteName;
                        cell.lblAddr.text = [NSString stringWithFormat:@"%@ %@, %@", newSite.siteAddressLine1, newSite.siteCity, newSite.siteState];
                        cell.lblPhone.text = [self formatPhoneNum:newSite.phone];
                    });
                }
                else
                {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        cell.lblCustName.text = @"";
                        cell.lblAddr.text = @"";
                        cell.lblPhone.text = @"";
                    });
                }
            });
        }
        else
        {
            cell.lblCustName.text = site.siteName;
            cell.lblAddr.text = [NSString stringWithFormat:@"%@ %@, %@", site.siteAddressLine1, site.siteCity, site.siteState];
            cell.lblPhone.text = [self formatPhoneNum:site.phone];
        }    

As you can see, if you dont already have the UserSite info for a task in _scannedSites, a background thread gets kicked off which gets the UserSite for that task, stores it, and then on the main thread fills in the details.

Like I said there is a pretty annoying lag when scrolling... which I hoped to avoid by doing the work in the background.

Am I going about this the wrong way?

Thanks, any advice is appreciated.


EDIT I created a relationship in CoreData and I am now using that in cellForRowAtIndexPath. If it does not exist yet, I create it. This is working much better.

    Site *site = task.site;
    if(!site)
    {
        AppDelegate *ad = [AppDelegate sharedAppDelegate];
        NSManagedObjectContext *temporaryContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        temporaryContext.persistentStoreCoordinator = ad.persistentStoreCoordinator;

        [temporaryContext performBlock:^{

            Site *tempContextSite = [task getSiteWithContext:temporaryContext];

            [ad.managedObjectContext performBlock:^{
                Site *mainManagedObject = (Site *)[ad.managedObjectContext objectWithID:tempContextSite.objectID];
                task.site = mainManagedObject;
                NSError *error;
                if (![temporaryContext save:&error])
                {
                }
                [ad.managedObjectContext performBlock:^{
                    NSError *e = nil;
                    if (![ad.managedObjectContext save:&e])
                    {

                    }
                    dispatch_async(dispatch_get_main_queue(), ^{
                        cell.lblCustName.text = mainManagedObject.siteName;
                        cell.lblAddr.text = [NSString stringWithFormat:@"%@ %@, %@", mainManagedObject.siteAddressLine1, mainManagedObject.siteCity, mainManagedObject.siteState];
                        cell.lblPhone.text = [self formatPhoneNum:mainManagedObject.phone];
                    });
                }];
            }];
        }];
    }
    else
    {
        cell.lblCustName.text = site.siteName;
        cell.lblAddr.text = [NSString stringWithFormat:@"%@ %@, %@", site.siteAddressLine1, site.siteCity, site.siteState];
        cell.lblPhone.text = [self formatPhoneNum:site.phone];
    }
RyanG
  • 4,393
  • 2
  • 39
  • 64

2 Answers2

1

If UserTask relates to UserSite, the usual Core Data approach would be to create a relationship between the two and then use that relationship at run time. So, UserTask would have a property named site, and you'd just ask a specific instance for the value of that property. An ID attribute might still exist but would only be used when syncing with some external data store (like a server API).

Storing IDs and looking up objects like this is a fundamentally awkward approach that's pretty much designed to do a lot of unnecessary work at run time. It avoids all of the conveniences that Core Data tries to provide, doing things the hard way instead. Doing this work while the table is scrolling is also about the worst possible time, because it's when a performance issue will be most noticeable.

If you must do it this way for some reason, you could optimize things by looking up all of the UserSite instances in advance instead of while the table is scrolling. If you know all of the UserTask instances, go get all the sites in one call when the view loads.

Tom Harrington
  • 69,312
  • 10
  • 146
  • 170
  • I have looked into creating relationships... but I am working with a huge data set. When the app first runs, it downloads & inserts around 300k records of all different types (UserTask, UserAppointment, UserSite etc...). If I were to use relationships, I would need to create that relationship at the point of insertion wouldn't I? Which in turn would add time to that initial download/insert process... I may be completely wrong here but that was my assumption of relationships. – RyanG May 03 '13 at 19:40
  • Yes, that's how it works. You can do the work at import time or you can do it later. Personally I'd prefer a slightly slower import time to bad table scrolling performance. – Tom Harrington May 03 '13 at 19:43
  • So I ended up creating the relationship.. I am still doing the work in cellForRowAtIndexPath but instead of keeping a temporary dictionary of IDs around I am checking to see if the appointment.userSite exists.. if it doesn't I create it, working much better! – RyanG May 06 '13 at 14:03
1

It is a bad idea to send of asynchronous tasks in cellForRowAtIndexPath:. If the user scrolls there are going to be a whole bunch of threads created which are maybe not even necessary.

It would be much better to have a background process that fetches the information you want and then notifies the UI to update itself if needed. This is pretty standard stuff, you will find many examples for solid implementations easily.

Mundi
  • 79,884
  • 17
  • 117
  • 140
  • I had built this solution as well.. the issue I was having with this method was that if you ended up scrolling far down the table and got ahead of the background fetching.. than you have to sit there and wait for the UserSite data.. this is why I went towards the path of only fetching the data for the cells that were visible. – RyanG May 03 '13 at 19:28