Come filtrare NSFetchedResultsController (CoreData) con UISearchDisplayController / UISearchBar


146

Sto cercando di implementare il codice di ricerca nella mia app per iPhone basata su CoreData. Non sono sicuro di come procedere. L'app ha già un NSFetchedResultsController con un predicato per recuperare i dati per il TableView primario. Voglio assicurarmi di essere sulla strada giusta prima di cambiare troppo codice. Sono confuso perché molti degli esempi sono basati su array anziché su CoreData.

Ecco alcune domande:

  1. Devo avere un secondo NSFetchedResultsController che recupera solo gli elementi corrispondenti o posso usare lo stesso del TableView primario?

  2. Se uso lo stesso, è semplice come svuotare la cache FRC e quindi modificare il predicato nel metodo handleSearchForTerm: searchString? Il predicato deve contenere il predicato iniziale nonché i termini di ricerca o ricorda che ha usato un predicato per recuperare i dati?

  3. Come posso tornare ai risultati originali? Ho appena impostato il predicato di ricerca su zero? Non ucciderà il predicato originale che è stato utilizzato per recuperare i risultati della FRC in primo luogo?

Se qualcuno ha qualche esempio di codice che utilizza la ricerca con FRC, lo apprezzerei molto!


@Brent, soluzione perfetta, ha funzionato a meraviglia per me!
DetartrateD

Risposte:


193

In realtà l'ho appena implementato su uno dei miei progetti (la tua domanda e l'altra risposta sbagliata suggerivano cosa fare). Ho provato la risposta di Sergio, ma ho riscontrato problemi di eccezione durante l'esecuzione su un dispositivo.

Sì, crei due controller di risultati del recupero: uno per la visualizzazione normale e un altro per la vista tabella di UISearchBar.

Se si utilizza solo un FRC (NSFetchedResultsController), l'UITableView originale (non la vista della tabella di ricerca attiva durante la ricerca) avrà probabilmente richiamate durante la ricerca e tenterà di utilizzare in modo errato la versione filtrata del FRC e verranno visualizzate le eccezioni generato dal numero errato di sezioni o righe nelle sezioni.

Ecco cosa ho fatto: ho due FRC disponibili come proprietà fetchedResultsController e searchFetchedResultsController. SearchFetchedResultsController non deve essere utilizzato a meno che non sia presente una ricerca (quando la ricerca viene annullata, è possibile vedere di seguito che questo oggetto viene rilasciato). Tutti i metodi UITableView devono capire da quale vista tabella verrà interrogata e da quale FRC applicabile estrarre le informazioni. I metodi delegati FRC devono anche capire quale tableView aggiornare.

È sorprendente quanto di tutto questo sia il codice boilerplate.

Bit rilevanti del file di intestazione:

@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;

bit rilevanti del file di implementazione:

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

Ho creato un metodo utile per recuperare il FRC corretto quando si lavora con tutti i metodi 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;

}

Metodi delegati per la barra di ricerca:

#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;
}

assicurarsi di utilizzare la vista tabella corretta quando si ottengono aggiornamenti dai metodi delegati 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];
}

Altre informazioni sulla vista:

- (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;
    }
}

Codice di creazione 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];
}   

3
Sembra funzionare magnificamente! Grazie Brent! Mi piace in particolare il metodo fetchedResultsControllerForTableView:. Questo lo rende molto semplice!
jschmidt,

2
Codice ridicolmente buono. Come ha detto jschmidt, il metodo personalizzato "fetchedResultsControllerForTableView:" semplifica davvero l'intero processo.
Daniel Amitay,

Brent. Tu sei l'uomo. Ma ecco una nuova sfida per te. Implementazione di questo codice mediante elaborazione in background. Ho fatto qualche multi-threading minore di altre parti della mia app, ma questo è difficile (almeno per me). Penso che aggiungerebbe un'esperienza utente migliore. Sfida accettata?
jschmidt,

3
@BrentPriddy Grazie! Ho modificato il codice per modificare la richiesta di recupero invece di impostare searchFetchedResultsControllersu nilogni volta che cambia il testo di ricerca.
ma11hew28,

2
Nel tuo cellForRowAtIndexPath, non dovresti ottenere la cellula self.tableViewcome qualcuno ha indicato in questa domanda SO? In caso contrario, la cella personalizzata non viene visualizzata.
amb

18

Alcuni hanno commentato che questo può essere fatto con un singolo NSFetchedResultsController. Questo è quello che ho fatto, e qui ci sono i dettagli. Questa soluzione presuppone che desideri semplicemente filtrare la tabella e mantenere tutti gli altri aspetti (ordinamento, layout delle celle, ecc.) Dei risultati della ricerca.

Innanzitutto, definisci due proprietà nella UITableViewControllersottoclasse (con @synthesize e dealloc appropriati, se applicabile):

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

In secondo luogo, inizializzare la barra di ricerca nel viewDidLoad:metodo della UITableViewControllersottoclasse:

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];

In terzo luogo, implementare i UISearchDisplayControllermetodi delegati come questo:

// 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];
}

Infine, nel fetchedResultsControllermetodo cambia il NSPredicatedipendere se self.searchStringè definito:

-(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;
} 

1
Questa soluzione ha funzionato bene per me ed è molto più semplice. Grazie! Suggerirei solo di modificare "if (self.searchString)" con "if (self.searchString.length). Ciò impedisce l'arresto anomalo se si fa clic sulla vista tabella dopo aver avviato una ricerca ed eliminato la stringa dalla barra di ricerca.
Guto Araujo,

17

Mi ci sono voluti alcuni tentativi per farlo funzionare ...

La mia chiave di comprensione era rendermi conto che ci sono due tableview al lavoro qui. Uno gestito dal mio viewcontroller e uno gestito dal searchviewcontroller e quindi ho potuto testare per vedere quale è attivo e fare la cosa giusta. Anche la documentazione è stata utile:

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

Ecco cosa ho fatto -

Aggiunto il flag 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;

Aggiunta la sintesi nel file di implementazione.

Quindi ho aggiunto questi metodi per la ricerca:

#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;
}

Quindi in controllerWillChangeContent:

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

E controllerDidChangeContent:

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

Ed elimina la cache quando reimposta il predicato.

Spero che questo ti aiuti.


eppure non capisco, l'esempio sopra è molto buono, ma incompleto, ma la tua raccomandazione dovrebbe funzionare, ma non ...
Vladimir Stazhilov,

Potresti semplicemente controllare la vista della tabella attiva invece di usare un BOOL:if ( [self.tableView isEqual:self.searchDisplayController.searchResultsTableView] ) { ... }
rwyland

@rwyland - I miei test mostrano che self.tableview non è impostato su searchdisplaycontroller.searchresultstableview quando la ricerca è attiva. Questi non sarebbero mai uguali.
giff

5

Ho affrontato lo stesso compito e ho trovato IL MODO PIÙ SEMPLICE POSSIBILE per risolverlo. In breve: è necessario definire un altro metodo, molto simile a -fetchedResultsControllerun predicato composto personalizzato.

Nel mio caso personale il mio -fetchedResultsControlleraspetto è questo:

- (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;
}

Come puoi vedere, sto recuperando i client di un'agenzia filtrati per agency.server_idpredicato. Di conseguenza sto recuperando i miei contenuti in un tableView(tutto relativo all'implementazione tableViewe al fetchedResultsControllercodice è piuttosto standard). Per implementare searchFieldsto definendo un UISearchBarDelegatemetodo delegato. Lo sto innescando con il metodo di ricerca, ad esempio -reloadTableView:

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

e ovviamente definizione di -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];
}

Questo gruppo di codice è molto simile al primo, "standard" -fetchedResultsController MA nell'istruzione if-else qui è:

+andPredicateWithSubpredicates: - usando questo metodo possiamo impostare un predicato per salvare i risultati del nostro primo recupero principale in tableView

+orPredicateWithSubpredicates - utilizzando questo metodo filtriamo il recupero esistente per query di ricerca da searchBar

Alla fine sto impostando una serie di predicati come predicato composto per questo particolare recupero. E per predicati richiesti, O per facoltativo.

E questo è tutto! Non è necessario implementare altro. Buona programmazione!


5

Stai usando una ricerca dal vivo?

Se NON lo sei, probabilmente vuoi un array (o un NSFetchedResultsController) con le ricerche precedenti che hai usato, quando l'utente preme "cerca", dici a FetchedResults di cambiarne il predicato.

Ad ogni modo, dovrai ricostruire ogni volta i tuoi FetchedResults. Ti consiglio di utilizzare solo un NSFetchedResultsController, poiché dovrai duplicare molto il codice e non dovrai sprecare memoria in qualcosa che non stai mostrando.

Assicurati solo di avere una variabile "searchParameters" NSString e che il tuo metodo FetchedResults lo ricostruisca per te secondo necessità, usando i parametri di ricerca se disponibili, dovresti semplicemente fare:

a) imposta "searchParameters" su qualcosa (o zero, se vuoi tutti i risultati).

b) rilasciare e impostare su zero l'oggetto NSFetchedResultsController corrente.

c) ricaricare i dati della tabella.

Ecco un semplice codice:

- (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;    
}

Molto interessante! Ci proverò.
jschmidt,

Questo sembra funzionare ma non riesce quando la tabella viene popolata da FRC, searchTableView è una tabella diversa dalla vista della tabella principale utilizzata. I metodi delegato FRC si diffondono ovunque su un dispositivo con memoria insufficiente durante la ricerca e il tableView principale vuole ricaricare le celle.
Brent Priddy,

Qualcuno ha un link a un modello di progetto per questo? Sto trovando davvero difficile capire cosa va dove. Sarebbe molto bello avere un modello funzionante come riferimento.
RyeMAC3,

@Brent, dovresti verificare se questa è la vista tabella di ricerca che necessita di modifiche nei metodi dei delegati FRC - se lo fai e aggiorni la tabella giusta nei metodi dei delegati FRC e UITableView, allora tutto dovrebbe andare bene quando usi FRC sia per la vista tabella principale sia per cerca tableview.
Kervich,

@kervich Credo che stai descrivendo la mia risposta sopra o stai dicendo che puoi farlo con un solo FRC?
Brent Priddy,

5

Swift 3.0, UISearchController, NSFetchedResultsController e Core Data

Questo codice funzionerà su Swift 3.0 con Core Data! Avrai bisogno di un metodo delegato singolo e di alcune righe di codice per filtrare e cercare oggetti dal modello. Nulla sarà necessario se sono stati implementati tutti FRCe i loro delegatemetodi searchController.

Il UISearchResultsUpdatingmetodo del protocollo

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 {}
}

Questo è tutto! Spero che ti aiuti! Grazie


1

SWIFT 3.0

Usa un textField, UISearchDisplayController è obsoleto a partire da iOS 8, dovresti usare un UISearchController. Invece di occuparti di Search Controller, perché non crei il tuo meccanismo di ricerca? Puoi personalizzarlo di più e avere un maggiore controllo su di esso, senza preoccuparti che SearchController cambi e / o sia deprecato.

Questo metodo che uso funziona molto bene e non richiede molto codice. Tuttavia, richiede l'utilizzo di Core Data e l'implementazione di NSFetchedResultsController.

Innanzitutto, crea un oggetto TextField e registralo con un metodo:

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

Quindi crea il tuo metodo textFieldDidChange, descritto nel selettore quando è stata aggiunta la destinazione:

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

Quindi si desidera filtrare l'elenco nel filterList()metodo usando NSPredicate o predicato NSCompound se è più complesso. Nel mio metodo filterList, sto filtrando in base al nome dell'entità e al nome dell'oggetto "sottocategorie" di entità (una relazione da una a molte).

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")
            }
        }
    }
}

0

Penso che Luka abbia un approccio migliore per questo. Vedi LargeDataSetSample e la sua ragione

Non usa FetchedResultsController, ma usa la cache durante la ricerca, quindi i risultati della ricerca appaiono molto più velocemente quando l'utente digita di più in SearchBar

Ho usato il suo approccio nella mia app e funziona bene. Ricorda anche se vuoi lavorare con l'oggetto Model, renderlo il più semplice possibile, vedi la mia risposta su setPropertiesToFetch


0

Ecco un modo di gestire i risultati recuperati con più set di dati che è sia abbastanza semplice che generale da applicare quasi ovunque. Basta prendere i risultati principali su un array quando sono presenti alcune condizioni.

NSArray *results = [self.fetchedResultsController fetchedObjects];

Esegui una query sull'array eseguendo il ciclo attraverso di essa o qualsiasi altra cosa desideri per creare un sottoinsieme dei risultati principali recuperati. E ora puoi utilizzare il set completo o il sottoinsieme quando è presente una condizione.


0

Mi è piaciuto molto l'approccio di @Josh O'Connor in cui non usa un UISearchController. Questo controller (Xcode 9) presenta ancora un bug di layout che molti stanno cercando di risolvere.

Ho ripristinato l'utilizzo di a UISearchBarinvece di a UITextField, e funziona abbastanza bene. Il mio requisito per la ricerca / filtro è di produrre un NSPredicate. Questo viene passato all'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

Infine, collega la SearchBar al suo delegato.

Spero che questo aiuti gli altri


0

Semplice approccio per filtrare UITableView esistente usando CoreData e che è già ordinato come vuoi.

Questo letteralmente anche io 5 minuti per impostare e iniziare a lavorare.

Avevo già UITableViewutilizzato un CoreDatapopolato con Dati da iCloud e che ha interazioni utente piuttosto complicate e non volevo replicare tutto ciò per un UISearchViewController. Sono stato in grado di aggiungere semplicemente un predicato all'esistente FetchRequestgià utilizzato da FetchResultsControllere che filtra i dati già ordinati.

-(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];
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.