Как фильтровать NSFetchedResultsController (CoreData) с помощью UISearchDisplayController / UISearchBar

146

Я пытаюсь реализовать поисковый код в своем приложении для iPhone на основе CoreData. Я не уверен, как поступить. В приложении уже есть NSFetchedResultsController с предикатом для извлечения данных для основного TableView. Я хочу убедиться, что я на правильном пути, прежде чем менять слишком много кода. Я запутался, потому что многие примеры основаны на массивах, а не на CoreData.

Вот несколько вопросов:

  1. Нужен ли мне второй NSFetchedResultsController, который извлекает только совпадающие элементы, или я могу использовать тот же, что и основной TableView?

  2. Если я использую тот же самый, это так же просто, как очистить кэш FRC и затем изменить предикат в методе handleSearchForTerm: searchString? Должен ли предикат содержать исходный предикат, а также условия поиска, или он помнит, что он использовал предикат для извлечения данных в первую очередь?

  3. Как мне вернуться к исходным результатам? Должен ли я установить предикат поиска равным нулю? Разве это не убьет исходный предикат, который использовался для получения результатов FRC?

Если у кого-нибудь есть примеры кода с использованием поиска в FRC, я был бы очень признателен!

jschmidt
источник
@ Брент, идеальное решение, помогло мне!
DetartrateD

Ответы:

193

Я на самом деле только что реализовал это в одном из моих проектов (ваш вопрос и другой неправильный ответ подсказали, что делать). Я попробовал ответить Серхио, но у меня возникли проблемы с исключениями при работе на устройстве.

Да, вы создаете два контроллера результатов выборки: один для обычного отображения и другой для табличного представления UISearchBar.

Если вы используете только один FRC (NSFetchedResultsController), то исходный UITableView (а не табличное представление поиска, которое активно во время поиска) может вызвать обратные вызовы во время поиска и попытаться неправильно использовать отфильтрованную версию вашего FRC, и вы увидите исключения выброшено о неправильном количестве разделов или строк в разделах.

Вот что я сделал: у меня есть два FRC, доступные как свойства fetchedResultsController и searchFetchedResultsController. SearchFetchedResultsController не должен использоваться, если нет поиска (когда поиск отменен, вы можете увидеть ниже, что этот объект освобожден). Все методы UITableView должны выяснить, из какого табличного представления он будет запрашивать и из какого применимого FRC извлекать информацию. Методы делегата FRC также должны выяснить, какой tableView следует обновить.

Удивительно, насколько это стандартный код.

Соответствующие биты заголовочного файла:

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

соответствующие биты файла реализации:

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

Я создал полезный метод для получения правильного FRC при работе со всеми методами 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;

}

Методы делегирования для панели поиска:

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

убедитесь, что вы используете правильное табличное представление при получении обновлений от методов делегата 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];
}

Другая информация для просмотра:

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

Код создания 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
Кажется, это прекрасно работает! Спасибо, Брент! Мне особенно нравится fetchedResultsControllerForTableView: метод. Это делает это очень легко!
jschmidt
2
Смешно хороший код. Как сказал jschmidt, пользовательский метод fetchedResultsControllerForTableView: действительно упрощает весь процесс.
Даниэль Амитей
Brent. Ты человек. Но вот новый вызов для вас. Реализация этого кода с использованием фоновой обработки. Я выполнил небольшую многопоточность других частей моего приложения, но это сложно (по крайней мере, для меня). Я думаю, что это добавит приятного пользовательского опыта. Вызов принят?
jschmidt
3
@BrentPriddy Спасибо! Я переработан код , чтобы модифицировать Fetch Request вместо установки searchFetchedResultsControllerна nilкаждый раз , когда изменения текста поиска.
ma11hew28
2
По вашему cellForRowAtIndexPath, разве вы не должны получить клетку, self.tableViewкак кто-то указал в этом вопросе? Если вы этого не сделаете, пользовательская ячейка не отображается.
Amb
18

Некоторые отметили, что это можно сделать с помощью одного NSFetchedResultsController. Это то, что я сделал, и вот подробности. Это решение предполагает, что вы просто хотите отфильтровать таблицу и сохранить все другие аспекты (порядок сортировки, расположение ячеек и т. Д.) Результатов поиска.

Сначала определите два свойства в вашем UITableViewControllerподклассе (с соответствующими @synthesize и dealloc, если применимо):

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

Во-вторых, инициализируйте строку поиска в viewDidLoad:методе вашего UITableViewControllerподкласса:

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

В-третьих, реализуйте UISearchDisplayControllerметоды делегата следующим образом:

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

Наконец, в fetchedResultsControllerметоде измените NSPredicateзависимость, если self.searchStringопределено:

-(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
Это решение хорошо сработало для меня и намного проще. Спасибо! Я бы только предложил настроить 'if (self.searchString)' на 'if (self.searchString.length). Это предотвращает его сбой, если вы щелкнете по представлению таблицы после начала поиска и удаления строки из панели поиска.
Гуто Араужо
17

Мне понадобилось несколько попыток, чтобы заставить это работать ...

Моим ключом к пониманию было понимание того, что здесь работают два tableView. Один управляется моим viewcontroller, а другой - searchviewcontroller, а затем я могу проверить, какой из них активен, и делать правильные вещи. Документация тоже была полезна:

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

Вот что я сделал -

Добавлен флаг 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;

Добавлен синтез в файле реализации.

Затем я добавил эти методы для поиска:

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

Затем в controllerWillChangeContent:

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

И controllerDidChangeContent:

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

И удалите кеш при сбросе предиката.

Надеюсь это поможет.

Роб Коэн
источник
пока я не понимаю, приведенный выше пример очень хороший, но неполный, но ваша рекомендация должна работать, но она не работает ...
Владимир Стажилов
Вы можете просто проверить активное представление таблицы вместо использования BOOL:if ( [self.tableView isEqual:self.searchDisplayController.searchResultsTableView] ) { ... }
rwyland
@rwyland - Мое тестирование показывает, что self.tableview не установлен на searchdisplaycontroller.searchresultstableview, когда поиск активен. Они никогда не будут равны.
Giff
5

Я столкнулся с той же задачей и нашел самый простой способ ее решить. Коротко: вам нужно определить еще один метод, очень похожий на-fetchedResultsController пользовательский составной предикат.

В моем личном случае мой -fetchedResultsControllerвыглядит так:

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

Как видите, я выбираю клиентов одного агентства, отфильтрованных по agency.server_idпредикату. В результате я получаю свой контент в tableView(все, что связано с реализацией tableViewи fetchedResultsControllerкодом довольно стандартно). Для реализации searchFieldя определяю UISearchBarDelegateметод делегата. Я запускаю его методом поиска, скажем -reloadTableView:

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

и конечно определение -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];
}

Этот набор кода очень похож на первый, «стандартный» -fetchedResultsController НО внутри оператора if-else:

+andPredicateWithSubpredicates: - используя этот метод, мы можем установить предикат для сохранения результатов нашей основной первой выборки в tableView

+orPredicateWithSubpredicates - используя этот метод, мы фильтруем существующую выборку по поисковому запросу из searchBar

В конце я устанавливаю массив предикатов как составной предикат для этой конкретной выборки. И для обязательных предикатов, ИЛИ для необязательных.

И это все! Вам не нужно ничего больше реализовывать. Удачного кодирования!

Alex
источник
5

Вы используете живой поиск?

Если вы НЕ, возможно, вы хотите массив (или NSFetchedResultsController) с предыдущими поисками, которые вы использовали, когда пользователь нажимает «поиск», вы говорите своим FetchedResults изменить его предикат.

В любом случае вам нужно будет каждый раз перестраивать свои FetchedResults. Я рекомендую использовать только один NSFetchedResultsController, так как вам придется многократно дублировать код, и вам не нужно тратить память на то, что вы не показываете.

Просто убедитесь, что у вас есть переменная NSString "searchParameters", и ваш метод FetchedResults перестраивает ее для вас по мере необходимости, используя параметры поиска, если они доступны, вы должны просто сделать:

а) установить "searchParameters" на что-то (или ноль, если вы хотите, чтобы все результаты).

б) освободить и установить в ноль текущий объект NSFetchedResultsController.

в) перезагрузить данные таблицы.

Вот простой код:

- (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;    
}
Серхио Моура
источник
Очень интересно! Я попробую.
jschmidt
Кажется, что это работает, но не работает, когда ваша таблица заполнена FRC, searchTableView - это таблица, отличная от используемой вами основной таблицы. Методы делегата FRC рвутся повсюду, когда на устройстве с низким объемом памяти при поиске и основной tableView хочет перезагрузить ячейки.
Брент Придди
У кого-нибудь есть ссылка на шаблон проекта для этого? Мне очень трудно понять, что и куда. Было бы очень хорошо иметь рабочий шаблон в качестве ссылки.
RyeMAC3
@Brent, Вы должны проверить, требует ли это таблица поиска поиска в методах делегатов FRC - если вы делаете и обновляете правильную таблицу в методах делегатов FRC и UITableView, то все должно быть в порядке при использовании FRC как для основного вида таблицы, так и для поиск по таблице.
Kervich
@kervich Я полагаю, что вы описываете мой ответ выше или вы говорите, что можете сделать это только с одним FRC?
Брент Придди
5

Swift 3.0, UISearchController, NSFetchedResultsController и Core Data

Этот код будет работать на Swift 3.0 с Core Data! Вам понадобится один метод делегата и несколько строк кода для фильтрации и поиска объектов из модели. Ничего не понадобится, если вы реализовали все FRCи их delegateметоды, а также searchController.

Метод UISearchResultsUpdatingпротокола

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

Это оно! Надеюсь, это поможет вам! Спасибо

Mannopson
источник
1

SWIFT 3.0

Использование textField, UISearchDisplayController устарела, начиная с iOS 8, вам придется использовать UISearchController. Вместо того чтобы иметь дело с контроллером поиска, почему бы вам не создать свой собственный механизм поиска? Вы можете настроить его больше и иметь больше контроля над ним, и вам не нужно беспокоиться об изменении SearchController и / или об его устаревании.

Этот метод, который я использую, работает очень хорошо и не требует большого количества кода. Однако для этого необходимо использовать Core Data и реализовать NSFetchedResultsController.

Сначала создайте TextField и зарегистрируйте его с помощью метода:

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

Затем создайте свой метод textFieldDidChange, описанный в селекторе при добавлении цели:

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

Затем вы хотите отфильтровать список в filterList()методе, используя предикат NSPredicate или NSCompound, если он более сложный. В моем методе filterList я выполняю фильтрацию на основе имени объекта и имени объекта "subCategories" (отношение один ко многим).

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

Я думаю, что Лука лучше подходит для этого. Смотрите LargeDataSetSample и его причину

Он не использует FetchedResultsController, но использует кеш при поиске, поэтому результаты поиска появляются намного быстрее, когда пользователь вводит больше в SearchBar

Я использовал его подход в своем приложении, и он работает хорошо. Также помните, если вы хотите работать с объектом Model, сделайте его как можно более простым, см. Мой ответ о setPropertiesToFetch

onmyway133
источник
0

Вот способ обработки fetchedResults с несколькими наборами данных, который является простым и достаточно общим для применения практически в любом месте. Просто захватите ваши основные результаты в массив, когда какое-то условие присутствует.

NSArray *results = [self.fetchedResultsController fetchedObjects];

Сделайте запрос к массиву, просматривая его или все, что вы хотите, чтобы создать подмножество ваших основных fetchedResults. И теперь вы можете использовать полный набор или подмножество, когда присутствует какое-либо условие.

smileBot
источник
0

Мне очень понравился подход @Josh O'Connor, когда он не использует UISearchController. Этот контроллер все еще (Xcode 9) имеет ошибку компоновки, которую многие пытаются обойти.

Я вернулся к использованию UISearchBarвместо UITextField, и это работает довольно хорошо. Мое требование для поиска / фильтра заключается в создании NSPredicate. Это передается в 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

Наконец, подключите SearchBar к его делегату.

Я надеюсь, что это помогает другим

TheGeezer
источник
0

Простой подход для фильтрации существующего UITableView с использованием CoreData и который уже отсортирован, как вы хотите.

Это буквально слишком 5 минут, чтобы настроить и начать работать.

У меня было существующее UITableViewиспользование, CoreDataзаполненное данными из iCloud, которое имеет довольно сложные взаимодействия с пользователем, и я не хотел повторять все это для a UISearchViewController. Мне удалось просто добавить предикат к существующему, который FetchRequestуже используется FetchResultsControllerи который фильтрует уже отсортированные данные.

-(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];
}
Клифф Рибаудо
источник