Cómo filtrar NSFetchedResultsController (CoreData) con UISearchDisplayController / UISearchBar

146

Estoy tratando de implementar el código de búsqueda en mi aplicación para iPhone basada en CoreData. No estoy seguro de cómo proceder. La aplicación ya tiene un NSFetchedResultsController con un predicado para recuperar los datos para el TableView primario. Quiero asegurarme de que estoy en el camino correcto antes de cambiar demasiado código. Estoy confundido porque muchos de los ejemplos están basados ​​en matrices en lugar de CoreData.

Aquí hay algunas preguntas:

  1. ¿Necesito tener un segundo NSFetchedResultsController que recupere solo los elementos coincidentes o puedo usar el mismo que el TableView primario?

  2. Si uso el mismo, ¿es tan simple como borrar el caché de FRC y luego cambiar el predicado en el método handleSearchForTerm: searchString? ¿El predicado tiene que contener el predicado inicial así como los términos de búsqueda o recuerda que usó un predicado para recuperar datos en primer lugar?

  3. ¿Cómo vuelvo a los resultados originales? ¿Acabo de establecer el predicado de búsqueda en nulo? ¿No matará eso el predicado original que se usó para recuperar los resultados de FRC en primer lugar?

Si alguien tiene algún ejemplo de código usando la búsqueda con el FRC, ¡lo agradecería mucho!

jschmidt
fuente
@Brent, solución perfecta, ¡fue un placer para mí!
Separado

Respuestas:

193

De hecho, acabo de implementar esto en uno de mis proyectos (su pregunta y la otra respuesta incorrecta insinuaron qué hacer). Intenté la respuesta de Sergio, pero tuve problemas de excepción cuando realmente se ejecuta en un dispositivo.

Sí, crea dos controladores de resultados de búsqueda: uno para la visualización normal y otro para la vista de tabla de UISearchBar.

Si solo usa un FRC (NSFetchedResultsController), entonces el UITableView original (no la vista de la tabla de búsqueda que está activa durante la búsqueda) posiblemente reciba devoluciones de llamada mientras está buscando e intente usar incorrectamente la versión filtrada de su FRC y verá excepciones arrojado sobre un número incorrecto de secciones o filas en secciones.

Esto es lo que hice: tengo dos FRC disponibles como propiedades fetchedResultsController y searchFetchedResultsController. SearchFetchedResultsController no debe usarse a menos que haya una búsqueda (cuando se cancela la búsqueda, puede ver a continuación que se libera este objeto). Todos los métodos UITableView deben determinar qué vista de tabla consultará y de qué FRC aplicable extraerá la información. Los métodos de delegado de FRC también deben determinar qué tableView actualizar.

Es sorprendente cuánto de esto es el código repetitivo.

Bits relevantes del archivo de encabezado:

@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 relevantes del archivo de implementación:

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

Creé un método útil para recuperar el FRC correcto al trabajar con todos los métodos 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;

}

Delegar métodos para la barra de búsqueda:

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

asegúrese de utilizar la vista de tabla correcta al obtener actualizaciones de los métodos de delegado de 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];
}

Otra información de la 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;
    }
}

Código de creación de 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
fuente
3
¡Eso parece funcionar maravillosamente! Gracias Brent! Particularmente me gusta el método fetchedResultsControllerForTableView:. ¡Eso lo hace muy fácil!
jschmidt
2
Ridículamente buen código. Como dijo jschmidt, el método personalizado "fetchedResultsControllerForTableView:" realmente simplifica todo el proceso.
Daniel Amitay
Brent Tu eres el hombre. Pero, aquí hay un nuevo desafío para ti. Implementación de este código utilizando procesamiento en segundo plano. He hecho algunos subprocesos múltiples de otras partes de mi aplicación, pero esto es difícil (al menos para mí). Creo que agregaría una mejor experiencia de usuario. ¿Desafío aceptado?
jschmidt
3
@BrentPriddy Gracias! Me refactorizado su código para modificar la solicitud Fetch en lugar de establecer la searchFetchedResultsControllerque nilcada vez que cambia el texto de búsqueda.
ma11hew28
2
En tu cellForRowAtIndexPath, ¿no deberías obtener la celda self.tableViewcomo alguien señalado en esta pregunta SO? Si no hace esto, la celda personalizada no se muestra.
amb
18

Algunos han comentado que esto se puede hacer con una sola NSFetchedResultsController. Eso es lo que hice, y aquí están los detalles. Esta solución supone que solo desea filtrar la tabla y mantener todos los demás aspectos (orden de clasificación, diseño de celda, etc.) de los resultados de búsqueda.

Primero, defina dos propiedades en su UITableViewControllersubclase (con los @synthesize y dealloc apropiados, si corresponde):

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

Segundo, inicialice la barra de búsqueda en el viewDidLoad:método de su UITableViewControllersubclase:

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

Tercero, implemente los UISearchDisplayControllermétodos delegados de esta manera:

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

Finalmente, en el fetchedResultsControllermétodo cambia el NSPredicatedependiente if self.searchStringestá definido:

-(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
fuente
1
Esta solución funcionó bien para mí y es mucho más simple. ¡Gracias! Solo sugeriría ajustar 'if (self.searchString)' a 'if (self.searchString.length). Esto evita que se bloquee si hace clic en la vista de tabla después de iniciar una búsqueda y eliminar la cadena de la barra de búsqueda.
Guto Araujo
17

Me tomó algunos intentos hacer que esto funcionara ...

Mi clave para entender fue darme cuenta de que hay dos tableViews en el trabajo aquí. Uno administrado por mi viewcontroller y otro administrado por searchviewcontroller y luego pude probar para ver cuál está activo y hacer lo correcto. La documentación también fue útil:

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

Esto es lo que hice.

Se agregó el indicador 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;

Se agregó la sintetización en el archivo de implementación.

Luego agregué estos métodos para buscar:

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

Luego, en controllerWillChangeContent:

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

Y controllerDidChangeContent:

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

Y elimine el caché al restablecer el predicado.

Espero que esto ayude.

Rob Cohen
fuente
sin embargo, yo no entiendo, el ejemplo anterior muy bueno, pero incompleto, pero su recomendación debería trabajar, pero no es así ...
Vladimir Stazhilov
Simplemente puede verificar la vista de tabla activa en lugar de usar un BOOL:if ( [self.tableView isEqual:self.searchDisplayController.searchResultsTableView] ) { ... }
rwyland
@rwyland: mis pruebas muestran que self.tableview no está configurado en searchdisplaycontroller.searchresultstableview cuando la búsqueda está activa. Estos nunca serían iguales.
Giff
5

Me enfrenté a la misma tarea y encontré EL MODO MÁS SENCILLO POSIBLE para resolverlo. En resumen: debe definir un método más, muy similar a -fetchedResultsControllerun predicado compuesto personalizado.

En mi caso personal, mi -fetchedResultsControlleraspecto es el siguiente:

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

Como puede ver, estoy buscando clientes de una agencia filtrada por agency.server_idpredicado. Como resultado, también estoy recuperando mi contenido en tableView(todo lo relacionado con la implementación tableViewy el fetchedResultsControllercódigo es bastante estándar). Para implementar searchFieldestoy definiendo un UISearchBarDelegatemétodo delegado. Lo estoy activando con el método de búsqueda, por ejemplo -reloadTableView:

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

y, por supuesto, la definición 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];
}

Este grupo de código es muy similar al primero, "estándar" -fetchedResultsController PERO dentro de la declaración if-else aquí es:

+andPredicateWithSubpredicates: - utilizando este método podemos establecer un predicado para guardar los resultados de nuestra primera búsqueda principal en el tableView

+orPredicateWithSubpredicates - usando este método, estamos filtrando la búsqueda existente por búsqueda de searchBar

Al final, estoy configurando una matriz de predicados como un predicado compuesto para esta búsqueda en particular. Y para predicados requeridos, O para opcional.

¡Y eso es todo! No necesita implementar nada más. ¡Feliz codificación!

Alex
fuente
5

¿Estás usando una búsqueda en vivo?

Si NO lo está, probablemente desee una matriz (o un NSFetchedResultsController) con las búsquedas anteriores que utilizó, cuando el usuario presiona "buscar", le dice a sus FetchedResults que cambien su predicado.

De cualquier manera, deberá reconstruir sus FetchedResults cada vez. Recomiendo usar solo un NSFetchedResultsController, ya que tendrás que duplicar mucho tu código y no necesitas desperdiciar memoria en algo que no estás mostrando.

Solo asegúrese de tener una variable NSString "searchParameters" y su método FetchedResults la reconstruya según sea necesario, utilizando los parámetros de búsqueda si están disponibles, simplemente debe hacer:

a) establezca los "parámetros de búsqueda" en algo (o nulo, si desea todos los resultados).

b) liberar y establecer en nulo el objeto NSFetchedResultsController actual.

c) recargar los datos de la tabla.

Aquí hay un código 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
fuente
¡Muy interesante! Lo intentaré.
jschmidt
Esto parece funcionar, pero falla cuando su tabla está poblada por el FRC, searchTableView es una tabla diferente de la vista de tabla principal que utiliza. Los métodos de delegado de FRC vomitan por todas partes cuando se está en un dispositivo con poca memoria cuando se busca y el tableView principal quiere recargar las celdas.
Brent Priddy
¿Alguien tiene un enlace a una plantilla de proyecto para esto? Me resulta muy difícil descubrir qué va a dónde. Sería muy bueno tener una plantilla de trabajo como referencia.
RyeMAC3
@Brent, debe verificar si esa es la vista de tabla de búsqueda que necesita cambios en los métodos de delegado de FRC; si lo hace y actualiza la tabla correcta en los métodos de delegados de FRC y UITableView, todo debería estar bien cuando se usa FRC tanto para la vista de tabla principal como para la vista de tabla principal. buscar tableview.
kervich
@kervich ¿Creo que está describiendo mi respuesta anterior o está diciendo que puede hacerlo con un solo FRC?
Brent Priddy
5

Swift 3.0, UISearchController, NSFetchedResultsController y Core Data

Este código funcionará en Swift 3.0 con Core Data! Necesitará un único método de delegado y algunas líneas de código para filtrar y buscar objetos del modelo. No se necesitará nada si ha implementado todos los métodos FRCy sus delegatemétodos, así como también searchController.

El UISearchResultsUpdatingmétodo de protocolo

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

¡Eso es! Espero que te ayude! Gracias

Mannopson
fuente
1

SWIFT 3.0

Use un campo de texto, UISearchDisplayController está en desuso a partir de iOS 8, tendría que usar un UISearchController. En lugar de tratar con el controlador de búsqueda, ¿por qué no crea su propio mecanismo de búsqueda? Puede personalizarlo más y tener más control sobre él, y no tener que preocuparse de que SearchController cambie o sea desaprobado.

Este método que utilizo funciona muy bien y no requiere mucho código. Sin embargo, requiere que use Core Data e implemente NSFetchedResultsController.

Primero, cree un TextField y regístrelo con un método:

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

Luego cree su método textFieldDidChange, descrito en el selector cuando se agregó el objetivo:

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

Luego, desea filtrar la lista en el filterList()método usando NSPredicate o NSCompound predicate si es más complejo. En mi método filterList, estoy filtrando según el nombre de la entidad y el nombre del objeto "subCategories" de las entidades (una relación de uno a muchos).

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
fuente
0

Creo que Luka tiene un mejor enfoque para esto. Ver LargeDataSetSample y su razón

No usa FetchedResultsController, pero usa caché cuando busca, por lo tanto, los resultados de búsqueda aparecen mucho más rápido cuando el usuario escribe más en SearchBar

He usado su enfoque en mi aplicación y funciona bien. Recuerde también que si desea trabajar con el objeto Modelo, hágalo lo más simple posible, vea mi respuesta sobre setPropertiesToFetch

onmyway133
fuente
0

Aquí hay una forma de manejar los resultados obtenidos con múltiples conjuntos de datos que es lo suficientemente simple y general como para aplicar en casi cualquier lugar. Simplemente tome sus resultados principales en una matriz cuando haya alguna condición presente.

NSArray *results = [self.fetchedResultsController fetchedObjects];

Consulte la matriz recorriéndola o lo que desee para crear un subconjunto de sus principales resultados obtenidos. Y ahora puede usar el conjunto completo o el subconjunto cuando existe alguna condición.

smileBot
fuente
0

Realmente me gustó el enfoque de @Josh O'Connor donde no usa a UISearchController. Este controlador todavía (Xcode 9) tiene un error de diseño que muchos están tratando de solucionar.

Volví a usar un en UISearchBarlugar de un UITextField, y funciona bastante bien. Mi requisito para la búsqueda / filtro es producir un NSPredicate. Esto se pasa al 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

Finalmente, conecte la barra de búsqueda a su delegado.

Espero que esto ayude a otros

TheGeezer
fuente
0

Enfoque simple para filtrar UITableView existente utilizando CoreData y que ya está ordenado como desee.

Esto literalmente también me lleva 5 minutos para configurar y trabajar.

Tenía un UITableViewuso existente CoreDatalleno de datos de iCloud y que tiene interacciones de usuario bastante complicadas y no quería tener que replicar todo eso por un UISearchViewController. Pude simplemente agregar un predicado al existente FetchRequestya utilizado por FetchResultsControllery que filtra los datos ya ordenados.

-(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];
}
Un acantilado
fuente