Search code examples
multithreadingcore-dataobjective-c-blocksgrand-central-dispatchuisearchdisplaycontroller

Updating UISearchDisplayController with Core Data results using GCD


I'm having trouble displaying the results from Core Data in my UISearchDisplayController when I implement GCD. Without it, it works, but obviously blocks the UI.

In my SearchTableViewController I have the following two methods:

- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
    // Tell the table data source to reload when text changes
    [self filterContentForSearchText:searchString];

    // Return YES to cause the search result table view to be reloaded.
    return YES;
}

// Update the filtered array based on the search text
-(void)filterContentForSearchText:(NSString*)searchText
{
    // Remove all objects from the filtered search array
    [self.filteredLocationsArray removeAllObjects];

    NSPredicate *predicate = [CoreDataMaster predicateForLocationUsingSearchText:@"Limerick"];

    CoreDataMaster *coreDataMaster = [[CoreDataMaster alloc] init];

    // Filter the array using NSPredicate
    self.filteredLocationsArray = [NSMutableArray arrayWithArray:
                                   [coreDataMaster fetchResultsFromCoreDataEntity:@"City" UsingPredicate:predicate]];

}

You can probably guess that my problem is with returning the array from [coreDataMaster fetchResultsFromCoreDataEntity]. Below is the method:

- (NSArray *)fetchResultsFromCoreDataEntity:(NSString *)entity UsingPredicate:(NSPredicate *)predicate
{    
    NSMutableArray *fetchedResults = [[NSMutableArray alloc] init];

    dispatch_queue_t coreDataQueue = dispatch_queue_create("com.coredata.queue", DISPATCH_QUEUE_SERIAL);

    dispatch_async(coreDataQueue, ^{


        NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
        NSEntityDescription *entityDescription = [NSEntityDescription
                                                  entityForName:entity inManagedObjectContext:self.managedObjectContext];

        NSSortDescriptor *nameSort = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
        NSArray *sortDescriptors = [NSArray arrayWithObjects:nameSort, nil];

        [fetchRequest setEntity:entityDescription];
        [fetchRequest setSortDescriptors:sortDescriptors];

        // Check if predicate is set
        if (predicate)
        {
            [fetchRequest setPredicate:predicate];
        }

        NSError *error = nil;

        NSArray *fetchedManagedObjects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];

        for (City *city in fetchedManagedObjects)
        {
            [fetchedResults addObject:city];
        }

        NSDictionary *userInfo = [NSDictionary dictionaryWithObject:[NSArray arrayWithArray:fetchedResults] forKey:@"results"];

        [[NSNotificationCenter defaultCenter]
         postNotificationName:@"fetchResultsComplete"
         object:nil userInfo:userInfo];

    });

    return [NSArray arrayWithArray:fetchedResults];
}

So the thread hasn't finished executing by the time it returns the results to self.filteredLocationsArray. I've tried added a NSNotification which passes the NSDictionary to this method:

- (void)updateSearchResults:(NSNotification *)notification
{
    NSDictionary *userInfo = notification.userInfo;
    NSArray *array = [userInfo objectForKey:@"results"];

    self.filteredLocationsArray = [NSMutableArray arrayWithArray:array];

    [self.tableView reloadData];
}

I've also tried refreshing the searchViewController like

[self.searchDisplayController.searchResultsTableView reloadData];

but to no avail. I'd really appreciate it if someone could point me in the right direction and show me where I might be going wrong.

Thanks

Edit

I just want to clarify something in Christopher's solution for future reference.

When I call this method, I put the GCD call to the main queue in the competition block. Also note the change to reload the tableView.

Example

[coreDataMaster fetchResultsFromCoreDataEntity:(NSString *)entity usingPredicate:(NSPredicate *)predicate completionHandler:^(NSArray *cities){

    dispatch_async(dispatch_get_main_queue(), ^{
        self.filteredLocationsArray = cities;
        [self.searchDisplayController.searchResultsTableView reloadData];
    });

}];

Solution

  • You've got a couple of issues.

    First, you're populating the fetchedResults array in an asynchronous callback. fetchResultsFromCoreDataEntity:usingPredicate: returns immediately, before the fetch happens. The usual way to handle this would be to add a completion handler block:

    -(void)fetchResultsFromCoreDataEntity:(NSString *)entity usingPredicate:(NSPredicate *)predicate completionHandler:(void (^)(NSArray *))completionHandler {    
        // create this queue in your init and re-use it
        // dispatch_queue_t coreDataQueue = dispatch_queue_create("com.coredata.queue", DISPATCH_QUEUE_SERIAL);
        dispatch_async(coreDataQueue, ^{
            NSMutableArray *fetchedResults = [NSMutableArray array];
            NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
            NSEntityDescription *entityDescription = [NSEntityDescription
                                                      entityForName:entity inManagedObjectContext:self.managedObjectContext];
    
            NSSortDescriptor *nameSort = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
            NSArray *sortDescriptors = [NSArray arrayWithObjects:nameSort, nil];
    
            [fetchRequest setEntity:entityDescription];
            [fetchRequest setSortDescriptors:sortDescriptors];
    
            // Check if predicate is set
            if (predicate != nil) {
                [fetchRequest setPredicate:predicate];
            }
    
            NSError *error = nil;
            NSArray *fetchedManagedObjects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
    
            for (City *city in fetchedManagedObjects) {
                [fetchedResults addObject:city];
            }
    
            if(completionHandler != nil) completionHandler(fetchedResults);
        });
    }
    

    Second, you're passing core data objects from one thread to another. Core Data is not thread safe, and doing this will almost certainly lead to crashes that seem random and are very hard to reproduce--I've seen it. Somewhere on your background thread, whether inside fetchResultsFromCoreDataEntity:usingPredicate:completionHandler: or in the completionHandler block, you should map the fields you need from your core data objects to vanilla value objects, and pass those off to the main thread. Something like this:

    NSMutableArray *cities = [NSMutableArray array];
    for (City *city in fetchedResults) {
        CityFields *cityFields = [[CityFields alloc] init];
        cityFields.name = city.name;
        cityFields.population = city.population;
        [cities addObject:cityFields];
    }
    
    dispatch_async(dispatch_get_main_queue(), ^{
        self.filteredLocationsArray = cities;
        [self.tableView reloadData];
    });