Search code examples
iosuitableviewnsfetchedresultscontrolleruisearchdisplaycontrollermagicalrecord

One NSFetchedResultController for UITableview and UISearchDisplayController


Maybe it's a silly question but I have a feeling that I can improve my project or at least make it more readable.

Here's the deal. I have a UITableView populated with data from server. For everything I use MagicalRecord and for dealing with UITableView I use NSFetchedResultController. And up to this point everything is fine. The problem occurs when I try to implement UISearchDisplayController. Insted of useing NSFetchedResultController I simply fetch data form Core Data and past it to the NSArray. Here is the method which I use for sorting:

- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope
{
    NSPredicate *resultPredicate = [NSPredicate predicateWithFormat:@"SELF.name contains[c] %@", searchText];
    searchResults = [Group MR_findAllSortedBy:@"caseInsensitiveName" ascending:YES withPredicate:resultPredicate];
}

An of course all UITableView boilerplate code is stuffed with if conditions checking type of the UITableView.
I think I can use one NSFetchedResultController to handle both tasks (populatind both UITableView's) and ommit this searchResult array but I'm not sure... So I would appreciate any suggestion, hint or whatever.

One more problem is when I use both UITableViews populated from other sources they collide when I enable sections. Simply, UISearchDisplayController.tableView is covered by sections from parent UITableView.

Edit

Here is some code used for handling searching and populating UITableView

#pragma mark - Table View

-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    // Count sections in FRC
    if (tableView == self.searchDisplayController.searchResultsTableView) {
        return 1;
    } else {
        return [[self.groupsFRC sections] count];
    }
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // Count groups in each section of FRC
    if (tableView == self.searchDisplayController.searchResultsTableView) {
        return [searchResults count];
    } else {
        return [[[self.groupsFRC sections] objectAtIndex:section] numberOfObjects];
    }
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Get reusable cell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"GroupCell"];

    // If there isn't any create new one
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"GroupCell"];
    }

    Group *group = [self determineGroupInTableViev:tableView atIndexPath:indexPath];

    cell.textLabel.text = group.name;

    // Checking if group has been selected earlier
    if ([self.selectedGroups containsObject:@{@"name" : group.name, @"id" : group.cashID}]) {
        [cell setAccessoryType:UITableViewCellAccessoryCheckmark];
    } else {
        [cell setAccessoryType:UITableViewCellAccessoryNone];
    }

    return cell;
}

// Adding checkmark to selected cell
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *selectedCell = [tableView cellForRowAtIndexPath:indexPath];
    Group *group = [self determineGroupInTableViev:tableView atIndexPath:indexPath];

    // Checking if selected cell has accessory view set to checkmark and add group to selected groups array
    if (selectedCell.accessoryType == UITableViewCellAccessoryNone)
    {
        selectedCell.accessoryType = UITableViewCellAccessoryCheckmark;
        [self.selectedGroups addObject:@{@"name" : group.name, @"id" : group.cashID}];
        NSLog(@"%@", self.selectedGroups);
    }
    else if (selectedCell.accessoryType == UITableViewCellAccessoryCheckmark)
    {
        selectedCell.accessoryType = UITableViewCellAccessoryNone;
        [self.selectedGroups removeObject:@{@"name" : group.name, @"id" : group.cashID}];
        NSLog(@"%@", self.selectedGroups);
    }

    // Hiding selection with animation for nice and clean effect
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
}

#pragma mark - Filtering

- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope
{
    NSPredicate *resultPredicate = [NSPredicate predicateWithFormat:@"SELF.name contains[c] %@", searchText];
    searchResults = [Group MR_findAllSortedBy:@"caseInsensitiveName" ascending:YES withPredicate:resultPredicate];
}

-(BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
    [self filterContentForSearchText:searchString
                               scope:[[self.searchDisplayController.searchBar scopeButtonTitles] objectAtIndex:[self.searchDisplayController.searchBar selectedScopeButtonIndex]]];
    return YES;
}

-(void)searchDisplayController:(UISearchDisplayController *)controller didHideSearchResultsTableView:(UITableView *)tableView
{
    // Realoading main table view when search result did hide
    [self.tableView reloadData];
}

Solution

  • I usually don't find UISearchDisplayController very useful. This is because:

    1. I already have a table view
    2. The display of the table view cells should be the same while searching and normal
    3. I still have to provide all of the table delegate and data source methods
    4. I still have to provide the search delegate methods

    So, I usually have a single source of data for the table view and manipulate that source based on and entered search criteria.

    This works for both FRC or simple Array. For the FRC, edit the fetch request (predicate) and performFetch:, everything else is automatic. For the Array, use sortedArrayUsingDescriptors: and reload (I have an original array of the source data, and another array reference used for the table methods. The second array either points to the original array instance or a new instance created from the sort).

    In both cases I have a similar amount of methods to using a UISearchDisplayController, but there are no if statements in the table methods. Any if statements and mutation of the data source and handled solely in the search methods (which are call much less frequently).


    Add a property to the class:

    @property (strong, nonatomic) NSPredicate *filterPredicate;
    

    In the search text changed delegate:

    if (text.length > 0) {
        self.filterPredicate = [NSPredicate predicateWithFormat:...];
    } else {
        self.filterPredicate = nil;
    }
    
    [self refreshFRC];
    

    And in the search cancel, just nil the predicate and refresh.

    And the refresh:

    - (void)refreshFRC {
        NSFetchRequest *fetch = ...;
    
        if (self.filterPredicate != nil) {
            fetch.predicate = self.filterPredicate;
        }
    
        self.frc.fetchRequest = fetch;
    
        [self.frc performFetch:...];
    }