Jak filtrować NSFetchedResultsController (CoreData) za pomocą UISearchDisplayController / UISearchBar

146

Próbuję zaimplementować kod wyszukiwania w mojej aplikacji na iPhone'a opartej na CoreData. Nie wiem, jak mam postępować. Aplikacja ma już NSFetchedResultsController z predykatem do pobierania danych dla podstawowego TableView. Chcę się upewnić, że jestem na właściwej ścieżce, zanim zmienię zbyt dużo kodu. Jestem zdezorientowany, ponieważ tak wiele przykładów jest opartych na tablicach, a nie na CoreData.

Oto kilka pytań:

  1. Czy muszę mieć drugi NSFetchedResultsController, który pobiera tylko pasujące elementy, czy mogę użyć tego samego, co podstawowy TableView?

  2. Jeśli użyję tego samego, czy jest to tak proste, jak wyczyszczenie pamięci podręcznej FRC, a następnie zmiana predykatu w metodzie handleSearchForTerm: searchString? Czy predykat musi zawierać początkowy predykat, a także wyszukiwane hasła, czy też pamięta, że ​​używał predykatu w celu pobrania danych w pierwszej kolejności?

  3. Jak wrócić do pierwotnych wyników? Czy po prostu ustawiam predykat wyszukiwania na zero? Czy to nie zabije pierwotnego predykatu, który został użyty do pobrania wyników FRC w pierwszej kolejności?

Jeśli ktoś ma jakieś przykłady kodu używającego wyszukiwania z FRC, byłbym bardzo wdzięczny!

jschmidt
źródło
@Brent, idealne rozwiązanie, sprawiło mi przyjemność!
DetartrateD

Odpowiedzi:

193

Właśnie zaimplementowałem to w jednym z moich projektów (Twoje pytanie i druga zła odpowiedź wskazywały, co mam robić). Wypróbowałem odpowiedź Sergio, ale miałem problemy z wyjątkami, gdy faktycznie działałem na urządzeniu.

Tak, tworzysz dwa kontrolery pobierania wyników: jeden do normalnego wyświetlania, a drugi do widoku tabeli UISearchBar.

Jeśli używasz tylko jednego FRC (NSFetchedResultsController), to oryginalny UITableView (nie widok tabeli wyszukiwania, który jest aktywny podczas wyszukiwania) prawdopodobnie będzie miał wywołania zwrotne podczas wyszukiwania i spróbuje nieprawidłowo użyć przefiltrowanej wersji twojego FRC, a zobaczysz wyjątki wyrzucony o nieprawidłowej liczbie sekcji lub wierszy w sekcjach.

Oto, co zrobiłem: mam dwa FRC dostępne jako właściwości pobraneResultsController i searchFetchedResultsController. Nie należy używać elementu searchFetchedResultsController, chyba że istnieje wyszukiwanie (gdy wyszukiwanie zostanie anulowane, poniżej możesz zobaczyć, że ten obiekt został zwolniony). Wszystkie metody UITableView muszą określić, do jakiego widoku tabeli będą kierowane zapytania i z którego odpowiedniego FRC mają pobierać informacje. Metody delegatów FRC muszą również określić, który tableView ma zostać zaktualizowany.

Zaskakujące jest, ile z tego stanowi kod standardowy.

Odpowiednie bity pliku nagłówkowego:

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

istotne fragmenty pliku implementacji:

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

Stworzyłem pomocną metodę pobierania poprawnego FRC podczas pracy ze wszystkimi metodami 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;

}

Metody delegowania dla paska wyszukiwania:

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

upewnij się, że używasz prawidłowego widoku tabeli podczas pobierania aktualizacji z metod delegata 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];
}

Inne informacje dotyczące widoku:

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

Kod tworzenia 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
źródło
3
Wydaje się, że działa pięknie! Dzięki, Brent! Szczególnie podoba mi się metoda FetchedResultsControllerForTableView:. To bardzo ułatwia!
jschmidt
2
Śmiesznie dobry kod. Jak powiedział jschmidt, niestandardowa metoda „fetchedResultsControllerForTableView:” naprawdę upraszcza cały proces.
Daniel Amitay,
Brent. Jesteś meżczyzną. Ale oto nowe wyzwanie dla Ciebie. Implementacja tego kodu przy użyciu przetwarzania w tle. Zrobiłem kilka drobnych wielowątkowości innych części mojej aplikacji, ale jest to trudne (przynajmniej dla mnie). Myślę, że dodałoby to przyjemniejsze wrażenia użytkownika. Wyzwanie przyjęte?
jschmidt
3
@BrentPriddy Thanks! Zreformowałem twój kod, aby zmodyfikować żądanie pobierania zamiast ustawiać na searchFetchedResultsControllerza nilkażdym razem, gdy zmienia się tekst wyszukiwania.
ma11hew28
2
Czy w twoim cellForRowAtIndexPath, czy nie powinieneś dostać komórki od self.tableViewkogoś, kto wskazał w tym pytaniu SO? Jeśli tego nie zrobisz, komórka niestandardowa nie zostanie wyświetlona.
amb
18

Niektórzy komentowali, że można to zrobić za pomocą jednego NSFetchedResultsController. Tak właśnie zrobiłem, a oto szczegóły. To rozwiązanie zakłada, że ​​chcesz po prostu przefiltrować tabelę i zachować wszystkie inne aspekty wyników wyszukiwania (porządek sortowania, układ komórek itp.).

Najpierw zdefiniuj dwie właściwości w swojej UITableViewControllerpodklasie (z odpowiednimi @synthesize i dealloc, jeśli dotyczy):

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

Po drugie, zainicjuj pasek wyszukiwania w viewDidLoad:metodzie Twojej UITableViewControllerpodklasy:

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

Po trzecie, zaimplementuj UISearchDisplayControllermetody delegatów w następujący sposób:

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

Na koniec w fetchedResultsControllermetodzie zmień NSPredicatezależność, jeśli self.searchStringjest zdefiniowane:

-(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
źródło
1
To rozwiązanie sprawdziło się u mnie i jest dużo prostsze. Dzięki! Sugerowałbym tylko zmianę „if (self.searchString)” na „if (self.searchString.length). Zapobiega to awarii, jeśli klikniesz widok tabeli po rozpoczęciu wyszukiwania i usunięciu ciągu z paska wyszukiwania.
Guto Araujo
17

Zajęło mi kilka prób, aby to zadziałało ...

Moim kluczem do zrozumienia było uświadomienie sobie, że działają tutaj dwa widoki tabeli. Jeden zarządzany przez mój kontroler widoku i jeden zarządzany przez searchviewcontroller, a następnie mogłem przetestować, który jest aktywny i zrobić to, co należy. Dokumentacja też była pomocna:

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

Oto, co zrobiłem -

Dodano flagę searchIsActive:

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

    NSString *sectionNameKeyPath;
    NSArray *sortDescriptors;


@private
    NSFetchedResultsController *fetchedResultsController_;
    NSManagedObjectContext *managedObjectContext_;

    BOOL searchIsActive;

}

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

Dodano syntezę w pliku implementacji.

Następnie dodałem te metody do wyszukiwania:

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

Następnie w controllerWillChangeContent:

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

I kontrolerDidChangeContent:

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

I usuń pamięć podręczną podczas resetowania predykatu.

Mam nadzieję że to pomoże.

Rob Cohen
źródło
jeszcze nie rozumiem, powyższy przykład bardzo dobry, ale niekompletny, ale twoja rekomendacja powinna działać, ale nie ...
Vladimir Stazhilov
Możesz po prostu sprawdzić aktywny widok tabeli zamiast używać BOOL:if ( [self.tableView isEqual:self.searchDisplayController.searchResultsTableView] ) { ... }
rwyland
@rwyland - Moje testy pokazują, że funkcja self.tableview nie jest ustawiona na searchdisplaycontroller.searchresultstableview, gdy wyszukiwanie jest aktywne. Te nigdy nie byłyby równe.
giff
5

Stałem przed tym samym zadaniem i znalazłem NAJPROSTSZY SPOSÓB na jego rozwiązanie. W skrócie: musisz zdefiniować jeszcze jedną metodę, bardzo podobną do tej -fetchedResultsControllerw przypadku niestandardowego predykatu złożonego.

W moim przypadku mój -fetchedResultsControllerwygląd wygląda tak:

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

Jak widać, pobieram klientów jednej agencji przefiltrowanych według agency.server_idpredykatu. W rezultacie pobieram zawartość w formacie tableView(wszystkie związane z implementacją tableViewi fetchedResultsControllerkodem są dość standardowe). Aby zaimplementować searchField, definiuję UISearchBarDelegatemetodę delegata. Uruchamiam to metodą wyszukiwania, powiedz -reloadTableView:

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

i oczywiście definicja -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];
}

Ten fragment kodu jest bardzo podobny do pierwszego, „standardowego”, -fetchedResultsController ALE wewnątrz instrukcji if-else jest:

+andPredicateWithSubpredicates: - za pomocą tej metody możemy ustawić predykat, aby zapisać wyniki naszego głównego pierwszego pobierania w formacie tableView

+orPredicateWithSubpredicates - za pomocą tej metody filtrujemy istniejące pobieranie według zapytania wyszukiwania searchBar

Na koniec ustawiam tablicę predykatów jako predykat złożony dla tego konkretnego pobrania. AND dla wymaganych predykatów, LUB dla opcjonalnych.

I to wszystko! Nie musisz niczego więcej wdrażać. Miłego kodowania!

Alex
źródło
5

Czy korzystasz z wyszukiwania na żywo?

Jeśli NIE jesteś, prawdopodobnie chcesz mieć tablicę (lub NSFetchedResultsController) z poprzednimi wyszukiwaniami, których używałeś, kiedy użytkownik naciśnie "szukaj", mówisz FetchedResults, aby zmienił swój predykat.

Tak czy inaczej, za każdym razem będziesz musiał odbudować swoje FetchedResults. Zalecam używanie tylko jednego kontrolera NSFetchedResultsController, ponieważ będziesz musiał dużo powielać kod i nie musisz marnować pamięci na coś, czego nie pokazujesz.

Po prostu upewnij się, że masz zmienną NSString "searchParameters", a metoda FetchedResults odbudowuje ją w razie potrzeby, używając parametrów wyszukiwania, jeśli są dostępne, po prostu wykonaj:

a) ustaw "searchParameters" na coś (lub nil, jeśli chcesz uzyskać wszystkie wyniki).

b) zwolnić i ustawić na zero bieżącego obiektu NSFetchedResultsController.

c) przeładuj dane tabeli.

Oto prosty kod:

- (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
źródło
Bardzo interesujące! Spróbuję.
jschmidt
Wydaje się, że to działa, ale kończy się niepowodzeniem, gdy tabela jest wypełniona przez FRC, searchTableView jest inną tabelą niż główny widok tabeli, którego używasz. Metody delegatów FRC rzygają wszędzie, gdy są na urządzeniu z małą ilością pamięci podczas wyszukiwania, a główna tabela tableView chce przeładować komórki.
Brent Priddy,
Czy ktoś ma link do szablonu projektu? Naprawdę trudno jest mi ustalić, co się dzieje. Byłoby bardzo dobrze mieć działający szablon jako odniesienie.
RyeMAC3
@Brent, Powinieneś sprawdzić, czy jest to widok tabeli wyszukiwania, który wymaga zmian w metodach delegatów FRC - jeśli to zrobisz i zaktualizujesz odpowiednią tabelę w metodach delegatów FRC i UITableView, wszystko powinno być w porządku, gdy używasz FRC zarówno dla głównego widoku tabeli, jak i szukaj w widoku tabeli.
kervich
@kervich Wydaje mi się, że opisujesz moją odpowiedź powyżej, czy też mówisz, że możesz to zrobić tylko z jednym FRC?
Brent Priddy
5

Swift 3.0, UISearchController, NSFetchedResultsController i Core Data

Ten kod będzie działał w Swift 3.0 z Core Data! Będziesz potrzebować jednej metody delegata i kilku wierszy kodu do filtrowania i wyszukiwania obiektów z modelu. Nic nie będzie potrzebne, jeśli zaimplementowałeś również wszystkie metody FRCi ich delegatemetodysearchController .

Metoda UISearchResultsUpdatingprotokołu

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

Otóż ​​to! Mam nadzieję, że ci to pomoże! Dzięki

Mannopson
źródło
1

SWIFT 3.0

Użyj textField, UISearchDisplayController jest przestarzały od iOS 8, musisz użyć UISearchController. Zamiast zajmować się kontrolerem wyszukiwania, dlaczego nie tworzysz własnego mechanizmu wyszukiwania? Możesz go bardziej dostosować i mieć nad nim większą kontrolę, bez martwienia się o zmianę i / lub wycofanie SearchController.

Ta metoda, której używam, działa bardzo dobrze i nie wymaga dużo kodu. Wymaga to jednak użycia podstawowych danych i implementacji NSFetchedResultsController.

Najpierw utwórz TextField i zarejestruj go metodą:

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

Następnie utwórz swoją metodę textFieldDidChange, opisaną w selektorze podczas dodawania celu:

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

Następnie chcesz przefiltrować listę w filterList()metodzie przy użyciu predykatu NSPredicate lub NSCompound, jeśli jest bardziej złożona. W mojej metodzie filterList filtruję na podstawie nazwy jednostki i nazwy obiektu „podkategorie” jednostek (relacja jeden do wielu).

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
źródło
0

Myślę, że Luka ma do tego lepsze podejście. Zobacz LargeDataSetSample i jego powód

Nie używa FetchedResultsController , ale używa pamięci podręcznej podczas wyszukiwania, stąd wyniki wyszukiwania pojawiają się znacznie szybciej, gdy użytkownik wpisze więcej w SearchBar

Użyłem jego podejścia w mojej aplikacji i działa dobrze. Pamiętaj również, że jeśli chcesz pracować z obiektem Model, uprość to tak, jak to tylko możliwe, zobacz moją odpowiedź na temat setPropertiesToFetch

onmyway133
źródło
0

Oto sposób obsługi funkcji fetchedResults z wieloma zestawami danych, który jest zarówno prosty, jak i wystarczająco ogólny, aby można go było zastosować prawie wszędzie. Po prostu przenieś swoje główne wyniki do tablicy, gdy występuje jakiś warunek.

NSArray *results = [self.fetchedResultsController fetchedObjects];

Zapytaj tablicę, przechodząc przez nią w pętli lub cokolwiek chcesz, aby utworzyć podzbiór swoich głównych pobieranych wyników. Teraz możesz użyć pełnego zestawu lub podzbioru, gdy występuje jakiś warunek.

smileBot
źródło
0

Bardzo podobało mi się podejście @Josh O'Connor, w którym nie używa UISearchController . Ten kontroler nadal (Xcode 9) ma błąd układu, który wielu próbuje obejść.

Wróciłem do używania a UISearchBarzamiast a UITextFieldi działa całkiem nieźle. Moje wymaganie dotyczące wyszukiwania / filtru polega na utworzeniu pliku NSPredicate. To jest przekazywane do 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

Na koniec połącz SearchBar z jego delegatem.

Mam nadzieję, że to pomoże innym

TheGeezer
źródło
0

Proste podejście do filtrowania istniejącego UITableView przy użyciu CoreData i które jest już posortowane w odpowiedni sposób.

To dosłownie dla mnie 5 minut na skonfigurowanie i rozpoczęcie pracy.

Miałem istniejące UITableViewużycie CoreDatawypełnione danymi z iCloud i które ma dość skomplikowane interakcje z użytkownikiem i nie chciałem powtarzać tego wszystkiego dla pliku UISearchViewController. Udało mi się po prostu dodać predykat do istniejącego FetchRequestjuż używanego przez FetchResultsControlleri który filtruje już posortowane dane.

-(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
źródło