Comment filtrer NSFetchedResultsController (CoreData) avec UISearchDisplayController / UISearchBar

146

J'essaie d'implémenter le code de recherche dans mon application iPhone basée sur CoreData. Je ne sais pas trop comment procéder. L'application dispose déjà d'un NSFetchedResultsController avec un prédicat pour récupérer les données pour le TableView principal. Je veux m'assurer que je suis sur la bonne voie avant de changer trop de code. Je suis confus parce que beaucoup d'exemples sont basés sur des tableaux au lieu de CoreData.

Voici quelques questions:

  1. Dois-je avoir un deuxième NSFetchedResultsController qui récupère uniquement les éléments correspondants ou puis-je utiliser le même que le TableView principal?

  2. Si j'utilise le même, est-ce aussi simple que de vider le cache FRC, puis de modifier le prédicat dans la méthode handleSearchForTerm: searchString? Le prédicat doit-il contenir le prédicat initial ainsi que les termes de recherche ou se souvient-il qu'il a utilisé un prédicat pour récupérer des données en premier lieu?

  3. Comment revenir aux résultats originaux? Dois-je simplement définir le prédicat de recherche sur nil? Cela ne tuera-t-il pas le prédicat original qui a été utilisé pour récupérer les résultats FRC en premier lieu?

Si quelqu'un a des exemples de code utilisant la recherche avec le FRC, je l'apprécierais grandement!

jschmidt
la source
@Brent, solution parfaite, a fonctionné un régal pour moi!
DetartrateD

Réponses:

193

En fait, je viens de l'implémenter sur l'un de mes projets (votre question et l'autre mauvaise réponse indiquaient quoi faire). J'ai essayé la réponse de Sergio mais j'ai eu des problèmes d'exception lors de l'exécution sur un appareil.

Oui, vous créez deux contrôleurs d'extraction de résultats: un pour l'affichage normal et un autre pour la vue de table de UISearchBar.

Si vous n'utilisez qu'un seul FRC (NSFetchedResultsController), l'UITableView d'origine (et non la vue de la table de recherche active lors de la recherche) aura peut-être des rappels appelés pendant que vous recherchez et essayez d'utiliser incorrectement la version filtrée de votre FRC et vous verrez des exceptions jeté sur un nombre incorrect de sections ou de lignes dans les sections.

Voici ce que j'ai fait: J'ai deux FRC disponibles en tant que propriétés fetchedResultsController et searchFetchedResultsController. Le searchFetchedResultsController ne doit pas être utilisé sauf s'il y a une recherche (lorsque la recherche est annulée, vous pouvez voir ci-dessous que cet objet est libéré). Toutes les méthodes UITableView doivent déterminer la vue de table qu'elle interrogera et le FRC applicable à partir duquel extraire les informations. Les méthodes de délégué FRC doivent également déterminer la tableView à mettre à jour.

Il est surprenant de constater à quel point cela est un code standard.

Bits pertinents du fichier d'en-tête:

@interface BlahViewController : UITableViewController <UISearchBarDelegate, NSFetchedResultsControllerDelegate, UISearchDisplayDelegate> 
{
    // other class ivars

    // required ivars for this example
    NSFetchedResultsController *fetchedResultsController_;
    NSFetchedResultsController *searchFetchedResultsController_;
    NSManagedObjectContext *managedObjectContext_;

    // The saved state of the search UI if a memory warning removed the view.
    NSString        *savedSearchTerm_;
    NSInteger       savedScopeButtonIndex_;
    BOOL            searchWasActive_;
}
@property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain, readonly) NSFetchedResultsController *fetchedResultsController;

@property (nonatomic, copy) NSString *savedSearchTerm;
@property (nonatomic) NSInteger savedScopeButtonIndex;
@property (nonatomic) BOOL searchWasActive;

bits pertinents du fichier d'implémentation:

@interface BlahViewController ()
@property (nonatomic, retain) NSFetchedResultsController *fetchedResultsController;
@property (nonatomic, retain) NSFetchedResultsController *searchFetchedResultsController;
@property (nonatomic, retain) UISearchDisplayController *mySearchDisplayController;
@end

J'ai créé une méthode utile pour récupérer le FRC correct lorsque vous travaillez avec toutes les méthodes UITableViewDelegate / DataSource:

- (NSFetchedResultsController *)fetchedResultsControllerForTableView:(UITableView *)tableView
{
    return tableView == self.tableView ? self.fetchedResultsController : self.searchFetchedResultsController;
}

- (void)fetchedResultsController:(NSFetchedResultsController *)fetchedResultsController configureCell:(UITableViewCell *)theCell atIndexPath:(NSIndexPath *)theIndexPath
{
    // your cell guts here
}

- (UITableViewCell *)tableView:(UITableView *)theTableView cellForRowAtIndexPath:(NSIndexPath *)theIndexPath
{
    CallTableCell *cell = (CallTableCell *)[theTableView dequeueReusableCellWithIdentifier:@"CallTableCell"];
    if (cell == nil) 
    {
        cell = [[[CallTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"CallTableCell"] autorelease];
    }

    [self fetchedResultsController:[self fetchedResultsControllerForTableView:theTableView] configureCell:cell atIndexPath:theIndexPath];
    return cell;
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 
{
    NSInteger count = [[[self fetchedResultsControllerForTableView:tableView] sections] count];

    return count;
}


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 
{
    NSInteger numberOfRows = 0;
    NSFetchedResultsController *fetchController = [self fetchedResultsControllerForTableView:tableView];
    NSArray *sections = fetchController.sections;
    if(sections.count > 0) 
    {
        id <NSFetchedResultsSectionInfo> sectionInfo = [sections objectAtIndex:section];
        numberOfRows = [sectionInfo numberOfObjects];
    }

    return numberOfRows;

}

Déléguer des méthodes pour la barre de recherche:

#pragma mark -
#pragma mark Content Filtering
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSInteger)scope
{
    // update the filter, in this case just blow away the FRC and let lazy evaluation create another with the relevant search info
    self.searchFetchedResultsController.delegate = nil;
    self.searchFetchedResultsController = nil;
    // if you care about the scope save off the index to be used by the serchFetchedResultsController
    //self.savedScopeButtonIndex = scope;
}


#pragma mark -
#pragma mark Search Bar 
- (void)searchDisplayController:(UISearchDisplayController *)controller willUnloadSearchResultsTableView:(UITableView *)tableView;
{
    // search is done so get rid of the search FRC and reclaim memory
    self.searchFetchedResultsController.delegate = nil;
    self.searchFetchedResultsController = nil;
}

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

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


- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchScope:(NSInteger)searchOption
{
    [self filterContentForSearchText:[self.searchDisplayController.searchBar text] 
                               scope:[self.searchDisplayController.searchBar selectedScopeButtonIndex]];

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

assurez-vous que vous utilisez la vue de table correcte lors de l'obtention des mises à jour des méthodes de délégué FRC:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller 
{
    UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
    [tableView beginUpdates];
}


- (void)controller:(NSFetchedResultsController *)controller 
  didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
           atIndex:(NSUInteger)sectionIndex 
     forChangeType:(NSFetchedResultsChangeType)type 
{
    UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;

    switch(type) 
    {
        case NSFetchedResultsChangeInsert:
            [tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}


- (void)controller:(NSFetchedResultsController *)controller 
   didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)theIndexPath 
     forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath 
{
    UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;

    switch(type) 
    {
        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:theIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeUpdate:
            [self fetchedResultsController:controller configureCell:[tableView cellForRowAtIndexPath:theIndexPath] atIndexPath:theIndexPath];
            break;

        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:theIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}


- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller 
{
    UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
    [tableView endUpdates];
}

Autres informations de vue:

- (void)loadView 
{   
    [super loadView];
    UISearchBar *searchBar = [[[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, self.tableView.frame.size.width, 44.0)] autorelease];
    searchBar.autoresizingMask = (UIViewAutoresizingFlexibleWidth);
    searchBar.autocorrectionType = UITextAutocorrectionTypeNo;
    self.tableView.tableHeaderView = searchBar;

    self.mySearchDisplayController = [[[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self] autorelease];
    self.mySearchDisplayController.delegate = self;
    self.mySearchDisplayController.searchResultsDataSource = self;
    self.mySearchDisplayController.searchResultsDelegate = self;
}

- (void)didReceiveMemoryWarning
{
    self.searchWasActive = [self.searchDisplayController isActive];
    self.savedSearchTerm = [self.searchDisplayController.searchBar text];
    self.savedScopeButtonIndex = [self.searchDisplayController.searchBar selectedScopeButtonIndex];

    fetchedResultsController_.delegate = nil;
    [fetchedResultsController_ release];
    fetchedResultsController_ = nil;
    searchFetchedResultsController_.delegate = nil;
    [searchFetchedResultsController_ release];
    searchFetchedResultsController_ = nil;

    [super didReceiveMemoryWarning];
}

- (void)viewDidDisappear:(BOOL)animated
{
    // save the state of the search UI so that it can be restored if the view is re-created
    self.searchWasActive = [self.searchDisplayController isActive];
    self.savedSearchTerm = [self.searchDisplayController.searchBar text];
    self.savedScopeButtonIndex = [self.searchDisplayController.searchBar selectedScopeButtonIndex];
}

- (void)viewDidLoad
{
    // restore search settings if they were saved in didReceiveMemoryWarning.
    if (self.savedSearchTerm)
    {
        [self.searchDisplayController setActive:self.searchWasActive];
        [self.searchDisplayController.searchBar setSelectedScopeButtonIndex:self.savedScopeButtonIndex];
        [self.searchDisplayController.searchBar setText:savedSearchTerm];

        self.savedSearchTerm = nil;
    }
}

Code de création FRC:

- (NSFetchedResultsController *)newFetchedResultsControllerWithSearch:(NSString *)searchString
{
    NSArray *sortDescriptors = // your sort descriptors here
    NSPredicate *filterPredicate = // your predicate here

    /*
     Set up the fetched results controller.
     */
    // Create the fetch request for the entity.
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    // Edit the entity name as appropriate.
    NSEntityDescription *callEntity = [MTCall entityInManagedObjectContext:self.managedObjectContext];
    [fetchRequest setEntity:callEntity];

    NSMutableArray *predicateArray = [NSMutableArray array];
    if(searchString.length)
    {
        // your search predicate(s) are added to this array
        [predicateArray addObject:[NSPredicate predicateWithFormat:@"name CONTAINS[cd] %@", searchString]];
        // finally add the filter predicate for this view
        if(filterPredicate)
        {
            filterPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:filterPredicate, [NSCompoundPredicate orPredicateWithSubpredicates:predicateArray], nil]];
        }
        else
        {
            filterPredicate = [NSCompoundPredicate orPredicateWithSubpredicates:predicateArray];
        }
    }
    [fetchRequest setPredicate:filterPredicate];

    // Set the batch size to a suitable number.
    [fetchRequest setFetchBatchSize:20];

    [fetchRequest setSortDescriptors:sortDescriptors];

    // Edit the section name key path and cache name if appropriate.
    // nil for section name key path means "no sections".
    NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest 
                                                                                                managedObjectContext:self.managedObjectContext 
                                                                                                  sectionNameKeyPath:nil 
                                                                                                           cacheName:nil];
    aFetchedResultsController.delegate = self;

    [fetchRequest release];

    NSError *error = nil;
    if (![aFetchedResultsController performFetch:&error]) 
    {
        /*
         Replace this implementation with code to handle the error appropriately.

         abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
         */
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    return aFetchedResultsController;
}    

- (NSFetchedResultsController *)fetchedResultsController 
{
    if (fetchedResultsController_ != nil) 
    {
        return fetchedResultsController_;
    }
    fetchedResultsController_ = [self newFetchedResultsControllerWithSearch:nil];
    return [[fetchedResultsController_ retain] autorelease];
}   

- (NSFetchedResultsController *)searchFetchedResultsController 
{
    if (searchFetchedResultsController_ != nil) 
    {
        return searchFetchedResultsController_;
    }
    searchFetchedResultsController_ = [self newFetchedResultsControllerWithSearch:self.searchDisplayController.searchBar.text];
    return [[searchFetchedResultsController_ retain] autorelease];
}   
Brent Priddy
la source
3
Cela semble fonctionner à merveille! Merci, Brent! J'aime particulièrement la méthode fetchedResultsControllerForTableView:. Cela rend les choses très faciles!
jschmidt
2
Code ridiculement bon. Comme jschmidt l'a dit, la méthode personnalisée "fetchedResultsControllerForTableView:" simplifie vraiment l'ensemble du processus.
Daniel Amitay
Brent. Tu es l'homme. Mais voici un nouveau défi pour vous. Implémentation de ce code en utilisant le traitement en arrière-plan. J'ai fait du multi-threading mineur sur d'autres parties de mon application, mais c'est difficile (du moins pour moi). Je pense que cela ajouterait une expérience utilisateur plus agréable. Défi accepté?
jschmidt
3
@BrentPriddy Merci! J'ai refactoré votre code pour modifier la demande de récupération au lieu de définir le searchFetchedResultsControllersur nilchaque fois que le texte de recherche change.
ma11hew28
2
Dans votre cellForRowAtIndexPath, ne devriez-vous pas obtenir la cellule de self.tableViewquelqu'un comme quelqu'un l'a indiqué dans cette question SO? Si vous ne le faites pas, la cellule personnalisée ne s'affiche pas.
amb
18

Certains ont fait remarquer que cela pouvait être fait avec un seul NSFetchedResultsController. C'est ce que j'ai fait, et voici les détails. Cette solution suppose que vous souhaitiez simplement filtrer le tableau et conserver tous les autres aspects (ordre de tri, disposition des cellules, etc.) des résultats de la recherche.

Tout d'abord, définissez deux propriétés dans votre UITableViewControllersous - classe (avec les @synthesize et dealloc appropriés, le cas échéant):

@property (nonatomic, retain) UISearchDisplayController *searchController;
@property (nonatomic, retain) NSString *searchString;

Deuxièmement, initialisez la barre de recherche dans la viewDidLoad:méthode de votre UITableViewControllersous-classe:

UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0,0,self.tableView.frame.size.width,44)]; 
searchBar.placeholder = @"Search";
searchBar.delegate = self;
self.searchController = [[[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self] autorelease];
self.searchController.delegate = self;
self.searchController.searchResultsDataSource = self;   
self.searchController.searchResultsDelegate = self; 
self.tableView.tableHeaderView = self.searchController.searchBar;
[searchBar release];

Troisièmement, implémentez les UISearchDisplayControllerméthodes de délégué comme ceci:

// This gets called when you start typing text into the search bar
-(BOOL)searchDisplayController:(UISearchDisplayController *)_controller shouldReloadTableForSearchString:(NSString *)_searchString {
   self.searchString = _searchString;
   self.fetchedResultsController = nil;
   return YES;
}

// This gets called when you cancel or close the search bar
-(void)searchDisplayController:(UISearchDisplayController *)controller willUnloadSearchResultsTableView:(UITableView *)tableView {
   self.searchString = nil;
   self.fetchedResultsController = nil;
   [self.tableView reloadData];
}

Enfin, dans la fetchedResultsControllerméthode changez le NSPredicatedépendant si self.searchStringest défini:

-(NSFetchedResultsController *)fetchedResultsController {
   if (fetchedResultsController == nil) {

       // removed for brevity

      NSPredicate *predicate;

      if (self.searchString) {
         // predicate that uses searchString (used by UISearchDisplayController)
         // e.g., [NSPredicate predicateWithFormat:@"name CONTAINS[cd] %@", self.searchString];
          predicate = ... 
      } else {
         predicate = ... // predicate without searchString (used by UITableViewController)
      }

      // removed for brevity

   }

   return fetchedResultsController;
} 
Chris
la source
1
Cette solution a bien fonctionné pour moi et c'est beaucoup plus simple. Merci! Je suggérerais seulement de modifier «if (self.searchString)» en «if (self.searchString.length). Cela l'empêche de planter si vous cliquez sur la vue du tableau après avoir lancé une recherche et supprimé la chaîne de la barre de recherche.
Guto Araujo du
17

Il m'a fallu quelques essais pour que cela fonctionne ...

Ma clé de compréhension a été de réaliser qu'il y a deux tablesViews à l'œuvre ici. Un géré par mon viewcontroller et un géré par le searchviewcontroller et ensuite je pourrais tester pour voir lequel est actif et faire la bonne chose. La documentation a également été utile:

http://developer.apple.com/library/ios/#documentation/uikit/reference/UISearchDisplayController_Class/Reference/Reference.html

Voici ce que j'ai fait -

Ajout du drapeau searchIsActive:

@interface ItemTableViewController : UITableViewController <NSFetchedResultsControllerDelegate, UISearchDisplayDelegate, UISearchBarDelegate> {

    NSString *sectionNameKeyPath;
    NSArray *sortDescriptors;


@private
    NSFetchedResultsController *fetchedResultsController_;
    NSManagedObjectContext *managedObjectContext_;

    BOOL searchIsActive;

}

@property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain) NSFetchedResultsController *fetchedResultsController;
@property (nonatomic, retain) NSString *sectionNameKeyPath;
@property (nonatomic, retain) NSArray *sortDescriptors;
@property (nonatomic) BOOL searchIsActive;

Ajout de la synthèse dans le fichier d'implémentation.

Ensuite, j'ai ajouté ces méthodes pour rechercher:

#pragma mark -
#pragma mark Content Filtering

- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope
{
    NSFetchRequest *aRequest = [[self fetchedResultsController] fetchRequest];

    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name BEGINSWITH[cd] %@", searchText];

    [aRequest setPredicate:predicate];

    NSError *error = nil;
    if (![[self fetchedResultsController] performFetch:&error]) {
        // Handle error
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }  

}

#pragma mark -
#pragma mark UISearchDisplayController Delegate Methods

- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
    [self filterContentForSearchText:[self.searchDisplayController.searchBar text] scope:nil];

    return YES;
}

/*
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchScope:(NSInteger)searchOption
{
    return YES;
}
*/

- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller {
    [self setSearchIsActive:YES];
    return;
}

- (void)searchDisplayControllerDidEndSearch:(UISearchDisplayController *)controller 
{
    NSFetchRequest *aRequest = [[self fetchedResultsController] fetchRequest];

    [aRequest setPredicate:nil];

    NSError *error = nil;
    if (![[self fetchedResultsController] performFetch:&error]) {
        // Handle error
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }  

    [self setSearchIsActive:NO];
    return;
}

Puis dans controllerWillChangeContent:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller 
{
    if ([self searchIsActive]) {
        [[[self searchDisplayController] searchResultsTableView] beginUpdates];
    }
    else  {
        [self.tableView beginUpdates];
    }
}

Et controllerDidChangeContent:

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller 
{
    if ([self searchIsActive]) {
        [[[self searchDisplayController] searchResultsTableView] endUpdates];
    }
    else  {
        [self.tableView endUpdates];
    }
}

Et supprimez le cache lors de la réinitialisation du prédicat.

J'espère que cela t'aides.

Rob Cohen
la source
pourtant je ne comprends pas, l'exemple ci-dessus est très bon, mais incomplet, mais votre recommandation devrait fonctionner, mais elle ne le fait pas ...
Vladimir Stazhilov
Vous pouvez simplement vérifier la vue de la table active au lieu d'utiliser un BOOL:if ( [self.tableView isEqual:self.searchDisplayController.searchResultsTableView] ) { ... }
rwyland
@rwyland - Mes tests montrent que self.tableview n'est pas défini sur searchdisplaycontroller.searchresultstableview lorsque la recherche est active. Celles-ci ne seraient jamais égales.
giff le
5

J'ai fait face à la même tâche et j'ai trouvé LE MOYEN LE PLUS SIMPLE pour le résoudre. En bref: vous devez définir une autre méthode, très similaire à celle -fetchedResultsControlleravec un prédicat composé personnalisé.

Dans mon cas personnel, je -fetchedResultsControllerressemble à ceci:

- (NSFetchedResultsController *) fetchedResultsController
{
    if (fetchedResultsController != nil)
    {
        return fetchedResultsController;
    }
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Client"
                                              inManagedObjectContext:[[PTDataManager sharedManager] managedObjectContext]];
    [fetchRequest setEntity:entity];

    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"agency_server_id == %@", agency.server_id];
    fetchRequest.predicate = predicate;

    NSSortDescriptor *sortByName1Descriptor = [[NSSortDescriptor alloc] initWithKey:@"lastname" ascending:YES];
    NSSortDescriptor *sortByName2Descriptor = [[NSSortDescriptor alloc] initWithKey:@"firstname" ascending:YES];
    NSSortDescriptor *sortByName3Descriptor = [[NSSortDescriptor alloc] initWithKey:@"middlename" ascending:YES];
    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects: sortByName1Descriptor, sortByName2Descriptor, sortByName3Descriptor, nil];

    fetchRequest.sortDescriptors = sortDescriptors;

    fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:fetchRequest managedObjectContext:[[PTDataManager sharedManager] managedObjectContext] sectionNameKeyPath:nil cacheName:nil];
    fetchedResultsController.delegate = self;
    return fetchedResultsController;
}

Comme vous pouvez le voir, je cherche les clients d'une agence filtrés par agency.server_idprédicat. En conséquence, je récupère mon contenu dans un tableView(tout lié à la mise en œuvre de tableViewet le fetchedResultsControllercode est assez standard) également. Pour implémenter, searchFieldje définis une UISearchBarDelegateméthode déléguée. Je le déclenche avec la méthode de recherche, disons -reloadTableView:

- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
    [self reloadTableView];
}

et bien sûr la définition de -reloadTableView:

- (void)reloadTableView
{
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Client"
                                              inManagedObjectContext:[[PTDataManager sharedManager] managedObjectContext]];
    [fetchRequest setEntity:entity];

    NSSortDescriptor *sortByName1Descriptor = [[NSSortDescriptor alloc] initWithKey:@"lastname" ascending:YES];
    NSSortDescriptor *sortByName2Descriptor = [[NSSortDescriptor alloc] initWithKey:@"firstname" ascending:YES];
    NSSortDescriptor *sortByName3Descriptor = [[NSSortDescriptor alloc] initWithKey:@"middlename" ascending:YES];
    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects: sortByName1Descriptor, sortByName2Descriptor, sortByName3Descriptor, nil];
    fetchRequest.sortDescriptors = sortDescriptors;

    NSPredicate *idPredicate = [NSPredicate predicateWithFormat:@"agency_server_id CONTAINS[cd] %@", agency.server_id];
    NSString *searchString = self.searchBar.text;
    if (searchString.length > 0)
    {
        NSPredicate *firstNamePredicate = [NSPredicate predicateWithFormat:@"firstname CONTAINS[cd] %@", searchString];
        NSPredicate *lastNamePredicate = [NSPredicate predicateWithFormat:@"lastname CONTAINS[cd] %@", searchString];
        NSPredicate *middleNamePredicate = [NSPredicate predicateWithFormat:@"middlename CONTAINS[cd] %@", searchString];
        NSPredicate *orPredicate = [NSCompoundPredicate orPredicateWithSubpredicates:[NSArray arrayWithObjects:firstNamePredicate, lastNamePredicate, middleNamePredicate, nil]];
        NSPredicate *andPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:idPredicate, nil]];
        NSPredicate *finalPred = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:orPredicate, andPredicate, nil]];
        [fetchRequest setPredicate:finalPred];
    }
    else
    {
        [fetchRequest setPredicate:idPredicate];
    }

    self.fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:fetchRequest managedObjectContext:[[PTDataManager sharedManager] managedObjectContext] sectionNameKeyPath:nil cacheName:nil];
    self.fetchedResultsController.delegate = self;

    NSError *error = nil;
    if (![self.fetchedResultsController performFetch:&error])
    {
        NSLog(@"Unresolved error %@, %@", [error localizedDescription], [error localizedFailureReason]);
    }; 

    [self.clientsTableView reloadData];
}

Ce groupe de code est très similaire au premier, "standard" -fetchedResultsController MAIS à l' intérieur de l'instruction if-else voici:

+andPredicateWithSubpredicates: - en utilisant cette méthode, nous pouvons définir un prédicat pour enregistrer les résultats de notre première extraction principale dans le tableView

+orPredicateWithSubpredicates - en utilisant cette méthode, nous filtrons l'extraction existante par requête de recherche à partir de searchBar

À la fin, je mets un tableau de prédicats en tant que prédicat composé pour cette extraction particulière. AND pour les prédicats requis, OU pour facultatif.

Et c'est tout! Vous n'avez rien d'autre à mettre en œuvre. Bon codage!

Alex
la source
5

Utilisez-vous une recherche en direct?

Si vous n'êtes PAS, vous voulez probablement un tableau (ou un NSFetchedResultsController) avec les recherches précédentes que vous avez utilisées, lorsque l'utilisateur appuie sur "recherche", vous dites à votre FetchedResults de changer son prédicat.

Dans tous les cas, vous devrez reconstruire vos FetchedResults à chaque fois. Je recommande d'utiliser un seul NSFetchedResultsController, car vous devrez beaucoup dupliquer votre code et vous n'avez pas besoin de gaspiller de la mémoire dans quelque chose que vous ne montrez pas.

Assurez-vous simplement que vous avez une variable NSString "searchParameters" et que votre méthode FetchedResults la reconstruit pour vous si nécessaire, en utilisant les paramètres de recherche si disponibles, vous devez simplement faire:

a) définissez "searchParameters" sur quelque chose (ou nul, si vous voulez tous les résultats).

b) relâchez et mettez à zéro l'objet NSFetchedResultsController actuel.

c) recharger les données de la table.

Voici un code simple:

- (void)searchString:(NSString*)s {
    self.searchResults = s;
    [fetchedResultsController release];
    fetchedResultsController = nil;
    [self.tableView reloadData];
}

-(NSFetchedResultsController *)fetchedResultsController {
    if (fetchedResultsController != nil) {
        return fetchedResultsController;
    }

    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"EntityName" inManagedObjectContext:self.context];
    [fetchRequest setEntity:entity];

    [fetchRequest setFetchBatchSize:20];

    // searchResults is a NSString*
    if (searchResults != nil) {
        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name LIKE %@",searchResults];
        [fetchRequest setPredicate:predicate];
    }

    fetchedResultsController = 
    [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest 
        managedObjectContext:self.context sectionNameKeyPath:nil 
        cacheName:nil];
    fetchedResultsController.delegate = self;

    [fetchRequest release];

    return fetchedResultsController;    
}
Sergio Moura
la source
Très intéressant! Je vais essayer.
jschmidt
Cela semble fonctionner mais il échoue lorsque votre table est remplie par le FRC, le searchTableView est une table différente de la vue de table principale que vous utilisez. Les méthodes de délégué FRC vomissent partout sur un appareil avec une mémoire insuffisante lors de la recherche et que le tableView principale souhaite recharger les cellules.
Brent Priddy
Quelqu'un a-t-il un lien vers un modèle de projet pour cela? J'ai vraiment du mal à comprendre ce qui va où. Ce serait très bien d'avoir un modèle de travail comme référence.
RyeMAC3
@Brent, Vous devriez vérifier si c'est la vue de table de recherche qui nécessite des modifications dans les méthodes de délégué FRC - si vous mettez à jour la bonne table dans les méthodes des délégués FRC et UITableView, tout devrait être ok lorsque vous utilisez FRC pour la vue de la table principale et la rechercher tableview.
kervich le
@kervich Je crois que vous décrivez ma réponse ci-dessus ou dites-vous que vous pouvez le faire avec un seul FRC?
Brent Priddy
5

Swift 3.0, UISearchController, NSFetchedResultsController et Core Data

Ce code fonctionnera sur Swift 3.0 avec Core Data! Vous aurez besoin d'une méthode de délégué unique et de quelques lignes de code pour filtrer et rechercher des objets à partir du modèle. Rien ne sera nécessaire si vous avez implémenté toutes les méthodes FRCet leurs delegateméthodes searchController.

La UISearchResultsUpdatingméthode du protocole

func updateSearchResults(for searchController: UISearchController) {

    let text = searchController.searchBar.text

    if (text?.isEmpty)! {
       // Do something 
    } else {
        self.fetchedResultsController.fetchRequest.predicate = NSPredicate(format: "( someString contains[cd] %@ )", text!)
    }
    do {
        try self.fetchedResultsController.performFetch()
        self.tableView.reloadData()
    } catch {}
}

C'est tout! J'espère que cela vous aide! Merci

Mannopson
la source
1

SWIFT 3.0

Utilisez un textField, UISearchDisplayController est obsolète à partir d'iOS 8, vous devrez utiliser un UISearchController. Au lieu de vous occuper du contrôleur de recherche, pourquoi ne créez-vous pas votre propre mécanisme de recherche? Vous pouvez le personnaliser davantage et avoir plus de contrôle dessus, sans avoir à vous soucier de la modification et / ou de la désapprobation de SearchController.

Cette méthode que j'utilise fonctionne très bien et ne nécessite pas beaucoup de code. Cependant, vous devez utiliser Core Data et implémenter NSFetchedResultsController.

Tout d'abord, créez un TextField et enregistrez-le avec une méthode:

searchTextField?.addTarget(self, action: #selector(textFieldDidChange), for: UIControlEvents.editingChanged)

Créez ensuite votre méthode textFieldDidChange, décrite dans le sélecteur lors de l'ajout de la cible:

func textFieldDidChange() {
    if let queryString = searchTextField.text {
        filterList(queryString)
        self.tableView.reloadData()
    }
}

Ensuite, vous voulez filtrer la liste dans la filterList()méthode à l'aide du prédicat NSPredicate ou NSCompound si elle est plus complexe. Dans ma méthode filterList, je filtre en fonction du nom de l'entité et du nom de l'objet "subCategories" d'entités (une relation un à plusieurs).

func filterList(_ queryString: String) {
    if let currentProjectID = Constants.userDefaults.string(forKey: Constants.CurrentSelectedProjectID) {
        if let currentProject = ProjectDBFacade.getProjectWithID(currentProjectID) {
            if (queryString != ""){
                let categoryPredicate = NSPredicate(format: "name CONTAINS[c] %@ && project == %@", queryString, currentProject)
                let subCategoryPredicate = NSPredicate(format: "subCategories.name CONTAINS[c] %@ && project == %@", queryString, currentProject)
                let orPredicate = NSCompoundPredicate(type: .or, subpredicates: [categoryPredicate, subCategoryPredicate])
                fetchedResultsController.fetchRequest.predicate = orPredicate
            }else{
                fetchedResultsController.fetchRequest.predicate = NSPredicate(format: "project == %@", currentProject)
            }

            do {
                try fetchedResultsController.performFetch()
            } catch {
                print("Error:  Could not fetch fetchedResultsController")
            }
        }
    }
}
Josh O'Connor
la source
0

Je pense que Luka a une meilleure approche pour cela. Voir LargeDataSetSample et sa raison

Il n'utilise pas FetchedResultsController, mais utilise le cache lors de la recherche, les résultats de la recherche apparaissent donc beaucoup plus rapidement lorsque l'utilisateur tape plus dans SearchBar

J'ai utilisé son approche dans mon application et cela fonctionne bien. Souvenez-vous également que si vous souhaitez travailler avec l'objet Model, rendez-le aussi simple que possible, voir ma réponse sur setPropertiesToFetch

onmyway133
la source
0

Voici une façon de gérer fetchedResults avec plusieurs ensembles de données qui est à la fois assez simple et générale pour s'appliquer presque partout. Saisissez simplement vos principaux résultats dans un tableau lorsqu'une condition est présente.

NSArray *results = [self.fetchedResultsController fetchedObjects];

Interrogez le tableau en le parcourant en boucle ou selon ce que vous désirez afin de créer un sous-ensemble de vos principaux fetchedResults. Et maintenant, vous pouvez utiliser l'ensemble complet ou le sous-ensemble lorsqu'une condition est présente.

smileBot
la source
0

J'ai beaucoup aimé l'approche de @Josh O'Connor où il n'utilise pas de fichier UISearchController. Ce contrôleur (Xcode 9) a toujours un bogue de mise en page que beaucoup tentent de contourner.

Je suis revenu à utiliser un UISearchBarau lieu d'un UITextField, et cela fonctionne très bien. Mon exigence pour la recherche / filtre est de produire un fichier NSPredicate. Ceci est transmis au FRC:

   class ViewController: UIViewController, UITableViewDelegate, 
 UITableViewDataSource, UISearchBarDelegate {

        @IBOutlet var searchBar: UISearchBar!

         func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {

                shouldShowSearchResults = true

                if let queryString = searchBar.text {
                    filterList(queryString)

                    fetchData()
                }
            }



          func filterList(_ queryString: String) {
        if (queryString == "") {
            searchPredicate = nil
    }
        else {
            let brandPredicate = NSPredicate(format: "brand CONTAINS[c] %@", queryString)
            let modelPredicate = NSPredicate(format: "model CONTAINS[c] %@", queryString)
            let orPredicate = NSCompoundPredicate(type: .or, subpredicates: [brandPredicate, modelPredicate])
            searchPredicate = orPredicate
    }

}

...

let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
        let request = NSFetchRequest<NSFetchRequestResult>(entityName: filmEntity)
        request.returnsDistinctResults = true
        request.propertiesToFetch = ["brand", "model"]

        request.sortDescriptors = [sortDescriptor]
        request.predicate = searchPredicate

Enfin, connectez SearchBar à son délégué.

J'espère que cela aide les autres

TheGeezer
la source
0

Approche simple pour filtrer UITableView existant à l'aide de CoreData et qui est déjà trié comme vous le souhaitez.

Cela m'a littéralement trop de 5 minutes pour configurer et travailler.

J'avais une UITableViewutilisation existante CoreDataremplie de données d'iCloud et qui a des interactions utilisateur assez compliquées et je ne voulais pas avoir à reproduire tout cela pour un UISearchViewController. J'ai pu simplement ajouter un prédicat à l'existant FetchRequestdéjà utilisé par le FetchResultsControlleret qui filtre les données déjà triées.

-(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
    NSPredicate *filterPredicate;

    if(searchText != nil && searchText.length > 0)
        filterPredicate = [NSPredicate predicateWithFormat:@"(someField CONTAINS[cd] %@) OR (someOtherField CONTAINS[cd] %@)", searchText, searchText];
    else
        filterPredicate = nil;

    _fetchedResultsController.fetchRequest.predicate = filterPredicate;

    NSError *error = nil;
    [_fetchedResultsController performFetch:&error];
    [self.tableView reloadData];
}
Cliff Ribaudo
la source