Como filtrar NSFetchedResultsController (CoreData) com UISearchDisplayController / UISearchBar

146

Estou tentando implementar o código de pesquisa no meu aplicativo para iPhone baseado em CoreData. Não tenho certeza de como proceder. O aplicativo já possui um NSFetchedResultsController com um predicado para recuperar os dados para o TableView primário. Quero ter certeza de que estou no caminho certo antes de alterar muito código. Estou confuso porque muitos dos exemplos são baseados em matriz, em vez de CoreData.

Aqui estão algumas perguntas:

  1. Preciso ter um segundo NSFetchedResultsController que recupere apenas os itens correspondentes ou posso usar o mesmo que o TableView principal?

  2. Se eu usar o mesmo, é tão simples quanto limpar o cache do FRC e alterar o predicado no método handleSearchForTerm: searchString? O predicado precisa conter o predicado inicial e os termos de pesquisa ou lembra que usou um predicado para recuperar dados em primeiro lugar?

  3. Como faço para voltar aos resultados originais? Acabei de definir o predicado de pesquisa como nulo? Isso não mata o predicado original usado para recuperar os resultados do FRC em primeiro lugar?

Se alguém tiver algum exemplo de código usando a pesquisa com o FRC, eu agradeceria muito!

jschmidt
fonte
@Brent, solução perfeita, funcionou um prazer para mim!
DetartrateD

Respostas:

193

Na verdade, acabei de implementar isso em um dos meus projetos (sua pergunta e a outra resposta errada sugeriram o que fazer). Tentei a resposta de Sergio, mas tive problemas de exceção ao executar em um dispositivo.

Sim, você cria dois controladores de resultados de busca: um para a exibição normal e outro para a exibição da tabela do UISearchBar.

Se você usar apenas um FRC (NSFetchedResultsController), o UITableView original (não a exibição da tabela de pesquisa que está ativa durante a pesquisa) possivelmente terá retornos de chamada chamados durante a pesquisa e tente usar incorretamente a versão filtrada do seu FRC e você verá exceções lançado sobre o número incorreto de seções ou linhas em seções.

Aqui está o que eu fiz: Eu tenho dois FRCs disponíveis como propriedades fetchedResultsController e searchFetchedResultsController. O searchFetchedResultsController não deve ser usado, a menos que haja uma pesquisa (quando a pesquisa é cancelada, você pode ver abaixo que este objeto foi lançado). Todos os métodos UITableView devem descobrir em qual exibição de tabela ele consultará e de qual FRC aplicável obter as informações. Os métodos de delegação do FRC também devem descobrir qual tableView atualizar.

É surpreendente quanto disso é um código padrão.

Bits relevantes do arquivo de cabeçalho:

@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 do arquivo de implementação:

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

Criei um método útil para recuperar o FRC correto ao trabalhar com todos os 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 a barra de pesquisa:

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

certifique-se de usar a exibição de tabela correta ao obter atualizações dos métodos de delegação do 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];
}

Outras informações de exibição:

- (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 criação 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
fonte
3
Isso parece funcionar lindamente! Obrigado, Brent! Eu particularmente gosto do método fetchedResultsControllerForTableView:. Isso torna muito fácil!
Jschmidt
2
Código ridiculamente bom. Como jschmidt disse, o método "fetchedResultsControllerForTableView:" personalizado realmente simplifica todo o processo.
Daniel Amitay
Brent. Você é o cara. Mas, aqui está um novo desafio para você. Implementação desse código usando o processamento em segundo plano. Fiz algumas pequenas tarefas múltiplas de outras partes do meu aplicativo, mas isso é difícil (pelo menos para mim). Eu acho que adicionaria uma experiência melhor ao usuário. Desafio aceito?
Jschmidt
3
@BrentPriddy Thanks! Refatorei seu código para Modificar a solicitação de busca em vez de defini-lo searchFetchedResultsControllerpara nilsempre que o texto da pesquisa for alterado.
ma11hew28
2
No seu cellForRowAtIndexPathcaso, você não deveria obter o celular self.tableViewcomo alguém apontado nesta pergunta do SO? Se você não fizer isso, a célula personalizada não será exibida.
amb
18

Alguns comentaram que isso pode ser feito com um único NSFetchedResultsController. Foi o que fiz e aqui estão os detalhes. Esta solução pressupõe que você deseja filtrar a tabela e manter todos os outros aspectos (ordem de classificação, layout da célula etc.) dos resultados da pesquisa.

Primeiro, defina duas propriedades em sua UITableViewControllersubclasse (com o @synthesize e o dealloc, se aplicável):

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

Segundo, inicialize a barra de pesquisa no viewDidLoad:método da sua UITableViewControllersubclasse:

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

Terceiro, implemente os UISearchDisplayControllermétodos delegados como este:

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

Por fim, no fetchedResultsControllermétodo, altere a NSPredicatedependência se self.searchStringfor definida:

-(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
fonte
1
Esta solução funcionou bem para mim e é muito mais simples. Obrigado! Eu sugeriria apenas ajustar 'if (self.searchString)' para 'if (self.searchString.length). Isso evita que ocorra uma falha se você clicar na visualização da tabela após iniciar uma pesquisa e excluir a sequência da barra de pesquisa.
Guto Araujo
17

Levei algumas tentativas para fazer isso funcionar ...

Minha chave para entender foi perceber que existem dois tableViews em funcionamento aqui. Um gerenciado pelo meu viewcontroller e outro gerenciado pelo searchviewcontroller e, em seguida, pude testar para ver qual está ativo e fazer a coisa certa. A documentação também foi útil:

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

Aqui está o que eu fiz -

Adicionado o sinalizador 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;

Adicionada a síntese no arquivo de implementação.

Em seguida, adicionei esses métodos para a pesquisa:

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

Em seguida, em controllerWillChangeContent:

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

And controllerDidChangeContent:

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

E exclua o cache ao redefinir o predicado.

Espero que isto ajude.

Rob Cohen
fonte
no entanto, eu não entendo, o exemplo acima é muito bom, mas incompleto, mas sua recomendação deve funcionar, mas não ...
Vladimir Stazhilov
Você pode apenas verificar a exibição de tabela ativa em vez de usar um BOOL:if ( [self.tableView isEqual:self.searchDisplayController.searchResultsTableView] ) { ... }
rwyland
@rwyland - Meus testes mostram que a self.tableview não está definida como searchdisplaycontroller.searchresultstableview quando a pesquisa está ativa. Estes nunca seriam iguais.
giff 6/09/13
5

Enfrentei a mesma tarefa e encontrei A MANEIRA MAIS SIMPLES POSSÍVEL para resolvê-la. Resumidamente: você precisa definir mais um método, muito semelhante a -fetchedResultsControllerum predicado composto personalizado.

No meu caso pessoal, minha -fetchedResultsControlleraparência é assim:

- (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 você pode ver, estou buscando clientes de uma agência filtrada por agency.server_idpredicado. Como resultado, também estou recuperando meu conteúdo em um tableView(tudo relacionado à implementação tableViewe ao fetchedResultsControllercódigo). Para implementar searchField, estou definindo um UISearchBarDelegatemétodo delegado. Estou ativando-o com o método de pesquisa, digamos -reloadTableView:

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

e, claro, a definição 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];
}

Esse monte de código é muito semelhante ao primeiro, "padrão", -fetchedResultsController MAS dentro da instrução if-else aqui é:

+andPredicateWithSubpredicates: - usando esse método, podemos definir um predicado para salvar os resultados da nossa primeira busca principal no tableView

+orPredicateWithSubpredicates - usando esse método, estamos filtrando a busca existente pela consulta de pesquisa de searchBar

No final, estou definindo uma matriz de predicados como predicado composto para essa busca em particular. E para predicados necessários, OU para opcional.

E isso é tudo! Você não precisa implementar mais nada. Feliz codificação!

Alex
fonte
5

Você está usando uma pesquisa ao vivo?

Se você NÃO é, provavelmente deseja uma matriz (ou um NSFetchedResultsController) com as pesquisas anteriores que você usou, quando o usuário pressiona "pesquisar", você diz ao FetchedResults para alterar seu predicado.

De qualquer forma, você precisará reconstruir seus FetchedResults sempre. Eu recomendo usar apenas um NSFetchedResultsController, pois você precisará duplicar muito seu código e não precisará desperdiçar memória em algo que não está mostrando.

Apenas verifique se você tem uma variável "searchParameters" do NSString e se o seu método FetchedResults a reconstrói conforme necessário, usando os parâmetros de pesquisa, se disponíveis, você deve:

a) defina os "parâmetros de pesquisa" para algo (ou nulo, se você quiser todos os resultados).

b) libere e defina para zero o objeto NSFetchedResultsController atual.

c) recarregar os dados da tabela.

Aqui está um código simples:

- (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
fonte
Muito interessante! Vou tentar.
Jschmidt
Isso parece funcionar, mas falha quando sua tabela é preenchida pelo FRC, o searchTableView é uma tabela diferente da exibição da tabela principal usada. Os métodos de delegação do FRC vomitam em todo o lugar quando em um dispositivo com pouca memória ao pesquisar e o tableView principal deseja recarregar células.
Brent Priddy
Alguém tem um link para um modelo de projeto para isso? Estou achando muito difícil descobrir o que vai aonde. Seria muito bom ter um modelo de trabalho como referência.
RyeMAC3 15/05
@Brent, você deve verificar se essa é a tabela de pesquisa que precisa de alterações nos métodos de delegação FRC - se você fizer e atualizar a tabela correta nos métodos dos delegados FRC e UITableView, tudo ficará bem ao usar o FRC para a tabela principal e a tabela pesquisa tableview.
26411 kervich
@ kervich Acredito que você esteja descrevendo minha resposta acima ou está dizendo que pode fazê-lo com apenas um FRC?
Brent Priddy
5

Swift 3.0, UISearchController, NSFetchedResultsController e Core Data

Este código funcionará no Swift 3.0 com Core Data! Você precisará de um único método delegado e de algumas linhas de código para filtrar e pesquisar objetos do modelo. Nada será necessário se você tiver implementado todos FRCe seus delegatemétodos, bem como searchController.

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

É isso aí! Espero que ajude você! obrigado

Mannopson
fonte
1

SWIFT 3.0

Use um textField, o UISearchDisplayController está obsoleto no iOS 8, você precisaria usar um UISearchController. Em vez de lidar com o Search Controller, por que você não cria seu próprio mecanismo de pesquisa? Você pode personalizá-lo mais e ter mais controle sobre ele, e não precisa se preocupar com a alteração e / ou descontinuação do SearchController.

Esse método que eu uso funciona muito bem e não requer muito código. No entanto, é necessário o uso de Dados Principais e a implementação do NSFetchedResultsController.

Primeiro, crie um TextField e registre-o com um método:

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

Em seguida, crie seu método textFieldDidChange, descrito no seletor quando o destino foi adicionado:

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

Em seguida, você deseja filtrar a lista no filterList()método usando o predicado NSPredicate ou NSCompound, se for mais complexo. No meu método filterList, estou filtrando com base no nome da entidade e no nome do objeto "subCategories" das entidades (um relacionamento de um para muitos).

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

Eu acho que Luka tem uma abordagem melhor para isso. Veja LargeDataSetSample e seu motivo

Ele não usa FetchedResultsController, mas usa cache ao pesquisar, portanto, os resultados da pesquisa aparecem muito mais rapidamente quando o usuário digita mais no SearchBar

Eu usei a abordagem dele no meu aplicativo e funciona bem. Lembre-se também de que, se você deseja trabalhar com o objeto Model, torne o mais simples possível, consulte minha resposta sobre setPropertiesToFetch

onmyway133
fonte
0

Aqui está uma maneira de lidar com os resultados buscados com vários conjuntos de dados, simples e gerais o suficiente para serem aplicados em praticamente qualquer lugar. Simplesmente pegue seus principais resultados em uma matriz quando alguma condição estiver presente.

NSArray *results = [self.fetchedResultsController fetchedObjects];

Consulte a matriz fazendo um loop através dela ou o que você desejar, a fim de criar um subconjunto de seus principais resultados buscados. E agora você pode usar o conjunto completo ou subconjunto quando alguma condição estiver presente.

smileBot
fonte
0

Eu realmente gostei da abordagem de @Josh O'Connor, onde ele não usa a UISearchController. Este controlador ainda (Xcode 9) possui um bug de layout que muitos estão tentando solucionar.

Voltei a usar um em UISearchBarvez de um UITextFielde funciona muito bem. Minha exigência para a pesquisa / filtro é produzir um NSPredicate. Isso é passado para o 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 o SearchBar ao seu delegado.

Espero que isso ajude outros

TheGeezer
fonte
0

Abordagem simples para filtrar o UITableView existente usando CoreData e que já está classificado como você deseja.

Isso literalmente também me 5 minutos para configurar e começar a trabalhar.

Eu já tinha um UITableViewuso CoreDatapreenchido com dados do iCloud e que tem interações bastante complicadas do usuário e não queria replicar tudo isso para a UISearchViewController. Consegui simplesmente adicionar um predicado ao FetchRequestjá existente usado pelo FetchResultsControllere que filtra os dados já classificados.

-(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
fonte