So filtern Sie NSFetchedResultsController (CoreData) mit UISearchDisplayController / UISearchBar


146

Ich versuche, Suchcode in meiner CoreData-basierten iPhone-App zu implementieren. Ich bin mir nicht sicher, wie ich vorgehen soll. Die App verfügt bereits über einen NSFetchedResultsController mit einem Prädikat zum Abrufen der Daten für die primäre TableView. Ich möchte sicherstellen, dass ich auf dem richtigen Weg bin, bevor ich zu viel Code ändere. Ich bin verwirrt, weil so viele Beispiele Array-basiert anstelle von CoreData sind.

Hier sind einige Fragen:

  1. Benötige ich einen zweiten NSFetchedResultsController, der nur die übereinstimmenden Elemente abruft, oder kann ich dasselbe wie die primäre TableView verwenden?

  2. Wenn ich dasselbe verwende, ist es so einfach, den FRC-Cache zu löschen und dann das Prädikat in der handleSearchForTerm: searchString-Methode zu ändern? Muss das Prädikat sowohl das ursprüngliche Prädikat als auch die Suchbegriffe enthalten, oder erinnert es sich daran, dass es überhaupt ein Prädikat zum Abrufen von Daten verwendet hat?

  3. Wie komme ich zu den ursprünglichen Ergebnissen zurück? Setze ich das Suchprädikat einfach auf Null? Wird das nicht das ursprüngliche Prädikat töten, das zum Abrufen der FRC-Ergebnisse verwendet wurde?

Wenn jemand Beispiele für Code hat, der die Suche mit dem FRC verwendet, würde ich mich sehr darüber freuen!


@Brent, perfekte Lösung, hat mir sehr gut gefallen!
DetartrateD

Antworten:


193

Ich habe dies gerade in einem meiner Projekte implementiert (Ihre Frage und die andere falsche Antwort deuteten an, was zu tun ist). Ich habe Sergios Antwort ausprobiert, hatte aber Ausnahmeprobleme, als ich tatsächlich auf einem Gerät lief.

Ja, Sie erstellen zwei Controller für Abrufergebnisse: einen für die normale Anzeige und einen für die Tabellenansicht der UISearchBar.

Wenn Sie nur einen FRC (NSFetchedResultsController) verwenden, werden in der ursprünglichen UITableView (nicht in der Suchtabellenansicht, die während der Suche aktiv ist) möglicherweise Rückrufe aufgerufen, während Sie suchen, und es wird versucht, die gefilterte Version Ihres FRC falsch zu verwenden, und es werden Ausnahmen angezeigt über falsche Anzahl von Abschnitten oder Zeilen in Abschnitten geworfen.

Folgendes habe ich getan: Ich habe zwei FRCs als Eigenschaften fetchedResultsController und searchFetchedResultsController verfügbar. Der searchFetchedResultsController sollte nur verwendet werden, wenn eine Suche stattfindet (wenn die Suche abgebrochen wird, können Sie unten sehen, dass dieses Objekt freigegeben ist). Alle UITableView-Methoden müssen herausfinden, welche Tabellenansicht abgefragt wird und aus welcher FRC die Informationen abgerufen werden sollen. Die FRC-Delegatmethoden müssen auch herausfinden, welche tableView aktualisiert werden soll.

Es ist überraschend, wie viel davon Boilerplate-Code ist.

Relevante Bits der Header-Datei:

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

relevante Bits der Implementierungsdatei:

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

Ich habe eine hilfreiche Methode erstellt, um die richtige FRC abzurufen, wenn ich mit allen UITableViewDelegate / DataSource-Methoden arbeite:

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

}

Delegieren Sie Methoden für die Suchleiste:

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

Stellen Sie sicher, dass Sie die richtige Tabellenansicht verwenden, wenn Sie Aktualisierungen von den FRC-Delegatenmethoden erhalten:

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

Weitere Ansichtsinformationen:

- (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-Erstellungscode:

- (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
Das scheint wunderbar zu funktionieren! Danke, Brent! Besonders gut gefällt mir die Methode fetchedResultsControllerForTableView:. Das macht es sehr einfach!
jschmidt

2
Lächerlich guter Code. Wie jschmidt sagte, vereinfacht die benutzerdefinierte Methode "fetchedResultsControllerForTableView:" den gesamten Prozess.
Daniel Amitay

Brent. Du bist der Mann. Aber hier ist eine neue Herausforderung für Sie. Implementierung dieses Codes mittels Hintergrundverarbeitung. Ich habe einige Teile meiner App geringfügig überarbeitet, aber das ist schwierig (zumindest für mich). Ich denke, es würde eine schönere Benutzererfahrung hinzufügen. Herausforderung angenommen?
jschmidt

3
@BrentPriddy Danke! Ich habe Ihren Code überarbeitet, um die Abrufanforderung zu ändern, anstatt ihn searchFetchedResultsControllerbei niljeder Änderung des Suchtextes auf zu setzen .
ma11hew28

2
Sollten cellForRowAtIndexPathSie in Ihrem Fall nicht die Zelle von self.tableViewjemandem bekommen, auf den in dieser SO-Frage hingewiesen wurde ? Wenn Sie dies nicht tun, wird die benutzerdefinierte Zelle nicht angezeigt.
Amb

18

Einige haben kommentiert, dass dies mit einem einzigen möglich ist NSFetchedResultsController. Das habe ich getan, und hier sind die Details. Bei dieser Lösung wird davon ausgegangen, dass Sie nur die Tabelle herausfiltern und alle anderen Aspekte (Sortierreihenfolge, Zellenlayout usw.) der Suchergebnisse beibehalten möchten.

Definieren Sie zunächst zwei Eigenschaften in Ihrer UITableViewControllerUnterklasse (ggf. mit dem entsprechenden @synthesize und Dealloc):

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

Zweitens initialisieren Sie die Suchleiste in der viewDidLoad:Methode Ihrer UITableViewControllerUnterklasse:

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

Drittens implementieren Sie die UISearchDisplayControllerDelegatmethoden wie folgt:

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

Schließlich wird in der fetchedResultsControllerMethode das NSPredicateabhängige if geändert 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
Diese Lösung hat bei mir gut funktioniert und ist viel einfacher. Vielen Dank! Ich würde nur vorschlagen, 'if (self.searchString)' auf 'if (self.searchString.length) zu ändern. Dies verhindert, dass es abstürzt, wenn Sie nach dem Starten einer Suche und dem Löschen der Zeichenfolge aus der Suchleiste auf die Tabellenansicht klicken.
Guto Araujo

17

Ich habe ein paar Versuche gebraucht, um das zum Laufen zu bringen ...

Mein Schlüssel zum Verständnis war die Erkenntnis, dass hier zwei tableViews am Werk sind. Eine von meinem Viewcontroller und eine vom Searchviewcontroller verwaltete und dann konnte ich testen, welche aktiv ist und das Richtige tun. Die Dokumentation war auch hilfreich:

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

Folgendes habe ich getan:

Das Flag searchIsActive wurde hinzugefügt:

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

Die Synthese in der Implementierungsdatei hinzugefügt.

Dann habe ich diese Methoden für die Suche hinzugefügt:

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

Dann in controllerWillChangeContent:

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

Und controllerDidChangeContent:

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

Löschen Sie den Cache, wenn Sie das Prädikat zurücksetzen.

Hoffe das hilft.


noch verstehe ich nicht, das obige Beispiel sehr gut, aber unvollständig, aber Ihre Empfehlung sollte funktionieren, aber es funktioniert nicht ...
Vladimir Stazhilov

Sie könnten einfach die aktive Tabellenansicht überprüfen, anstatt ein BOOL zu verwenden:if ( [self.tableView isEqual:self.searchDisplayController.searchResultsTableView] ) { ... }
rwyland

@rwyland - Meine Tests haben gezeigt, dass self.tableview nicht auf searchdisplaycontroller.searchresultstableview eingestellt ist, wenn die Suche aktiv ist. Diese würden niemals gleich sein.
Giff

5

Ich stand vor der gleichen Aufgabe und fand den einfachsten Weg, sie zu lösen. Kurz gesagt: Sie müssen eine weitere Methode definieren, die -fetchedResultsControllereinem benutzerdefinierten zusammengesetzten Prädikat sehr ähnlich ist .

In meinem persönlichen Fall -fetchedResultsControllersieht mein so aus:

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

Wie Sie sehen können, hole ich Kunden einer Agentur, die nach agency.server_idPrädikaten gefiltert sind . Infolgedessen rufe ich meine Inhalte auch in a ab tableView(alles, was mit der Implementierung von tableViewund fetchedResultsControllerCode zusammenhängt, ist ziemlich Standard). Zur Implementierung searchFielddefiniere ich eine UISearchBarDelegateDelegatenmethode. Ich löse es mit der Suchmethode aus, sagen wir -reloadTableView:

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

und natürlich Definition von -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];
}

Dieser Code-Haufen ist dem ersten "Standard" sehr ähnlich, -fetchedResultsController ABER in der if-else-Anweisung hier ist:

+andPredicateWithSubpredicates: - Mit dieser Methode können wir ein Prädikat festlegen, um die Ergebnisse unseres ersten Hauptabrufs im zu speichern tableView

+orPredicateWithSubpredicates - Mit dieser Methode filtern wir den vorhandenen Abruf nach Suchabfragen aus searchBar

Am Ende setze ich ein Array von Prädikaten als zusammengesetztes Prädikat für diesen bestimmten Abruf. UND für erforderliche Prädikate ODER für optional.

Und das ist alles! Sie müssen nichts mehr implementieren. Viel Spaß beim Codieren!


5

Verwenden Sie eine Live-Suche?

Wenn Sie NICHT sind, möchten Sie wahrscheinlich ein Array (oder einen NSFetchedResultsController) mit den zuvor verwendeten Suchvorgängen. Wenn der Benutzer auf "Suchen" drückt, weisen Sie Ihre FetchedResults an, sein Prädikat zu ändern.

In beiden Fällen müssen Sie Ihre FetchedResults jedes Mal neu erstellen. Ich empfehle, nur einen NSFetchedResultsController zu verwenden, da Sie Ihren Code häufig duplizieren müssen und keinen Speicher für etwas verschwenden müssen, das Sie nicht anzeigen.

Stellen Sie einfach sicher, dass Sie eine NSString-Variable "searchParameters" haben und Ihre FetchedResults-Methode sie nach Bedarf für Sie neu erstellt. Verwenden Sie dazu die Suchparameter, falls verfügbar. Führen Sie einfach Folgendes aus:

a) Setzen Sie die "Suchparameter" auf etwas (oder auf Null, wenn Sie alle Ergebnisse wünschen).

b) Lassen Sie das aktuelle NSFetchedResultsController-Objekt los und setzen Sie es auf Null.

c) Laden Sie die Tabellendaten neu.

Hier ist ein einfacher Code:

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

Sehr interessant! Ich werde es versuchen.
jschmidt

Dies scheint zu funktionieren, schlägt jedoch fehl, wenn Ihre Tabelle vom FRC gefüllt wird. Die searchTableView ist eine andere Tabelle als die von Ihnen verwendete Haupttabellenansicht. Die FRC-Delegatenmethoden kotzen überall auf einem Gerät mit wenig Speicher bei der Suche und die Haupttabellenansicht möchte Zellen neu laden.
Brent Priddy

Hat jemand einen Link zu einer Projektvorlage dafür? Es fällt mir wirklich schwer herauszufinden, was wohin geht. Es wäre sehr schön, eine Arbeitsvorlage als Referenz zu haben.
RyeMAC3

@Brent, Sie sollten überprüfen, ob dies die Suchtabellenansicht ist, die Änderungen an den FRC-Delegatenmethoden erfordert. Wenn Sie die richtige Tabelle in den FRC- und UITableView-Delegatenmethoden aktualisieren, sollte alles in Ordnung sein, wenn Sie FRC sowohl für die Haupttabellenansicht als auch für die verwenden Suche in der Tabellenansicht.
Kervich

@kervich Ich glaube, Sie beschreiben meine Antwort oben oder sagen Sie, dass Sie es mit nur einem FRC tun können?
Brent Priddy

5

Swift 3.0, UISearchController, NSFetchedResultsController und Core Data

Dieser Code funktioniert unter Swift 3.0 mit Core Data! Sie benötigen eine einzelne Delegatmethode und einige Codezeilen zum Filtern und Suchen von Objekten aus dem Modell. Es wird nichts benötigt, wenn Sie alle FRCund ihre delegateMethoden sowie implementiert haben searchController.

Die UISearchResultsUpdatingProtokollmethode

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

Das ist es! Hoffe es hilft dir! Vielen Dank


1

SWIFT 3.0

Verwenden Sie ein Textfeld. UISearchDisplayController ist ab iOS 8 veraltet. Sie müssten einen UISearchController verwenden. Warum erstellen Sie keinen eigenen Suchmechanismus, anstatt sich mit dem Suchcontroller zu befassen? Sie können es weiter anpassen und haben mehr Kontrolle darüber und müssen sich keine Sorgen machen, dass SearchController geändert und / oder veraltet wird.

Diese Methode, die ich verwende, funktioniert sehr gut und erfordert nicht viel Code. Sie müssen jedoch Core Data verwenden und NSFetchedResultsController implementieren.

Erstellen Sie zunächst ein TextField und registrieren Sie es mit einer Methode:

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

Erstellen Sie dann Ihre textFieldDidChange-Methode, die im Selektor beschrieben wurde, als das Ziel hinzugefügt wurde:

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

Anschließend möchten Sie die Liste in der filterList()Methode mithilfe des NSPredicate- oder NSCompound-Prädikats filtern, wenn sie komplexer ist. In meiner filterList-Methode filtere ich basierend auf dem Namen der Entität und dem Namen des Objekts "subCategories" der Entitäten (eine Eins-zu-Viele-Beziehung).

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

Ich denke, Luka hat dafür einen besseren Ansatz. Siehe LargeDataSetSample und seinen Grund

Er FetchedResultsControllerverwendet den Cache nicht , verwendet ihn jedoch bei der Suche. Daher werden die Suchergebnisse viel schneller angezeigt, wenn der Benutzer mehr in SearchBar eingibt

Ich habe seinen Ansatz in meiner App verwendet und es funktioniert OK. Denken Sie auch daran, wenn Sie mit dem Modellobjekt arbeiten möchten, machen Sie es so einfach wie möglich. Lesen Sie meine Antwort zu setPropertiesToFetch


0

Hier ist eine Möglichkeit, fetchedResults mit mehreren Datensätzen zu behandeln, die sowohl einfach als auch allgemein genug ist, um fast überall angewendet zu werden. Rufen Sie einfach Ihre Hauptergebnisse in einem Array auf, wenn eine Bedingung vorliegt.

NSArray *results = [self.fetchedResultsController fetchedObjects];

Fragen Sie das Array ab, indem Sie es durchlaufen oder was auch immer Sie möchten, um eine Teilmenge Ihrer wichtigsten fetchedResults zu erstellen. Und jetzt können Sie entweder die vollständige Menge oder die Teilmenge verwenden, wenn eine Bedingung vorliegt.


0

Ich mochte @Josh O'Connors Ansatz, bei dem er kein a verwendet UISearchController. Dieser Controller (Xcode 9) weist immer noch einen Layoutfehler auf, den viele zu umgehen versuchen.

Ich habe wieder a UISearchBaranstelle von a verwendet UITextField, und es funktioniert ganz gut. Meine Anforderung für die Suche / Filter ist es, eine zu erstellen NSPredicate. Dies wird an die FRC weitergegeben:

   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

Verbinden Sie abschließend die SearchBar mit ihrem Delegaten.

Ich hoffe das hilft anderen


0

Einfacher Ansatz zum Filtern vorhandener UITableView mithilfe von CoreData, der bereits nach Ihren Wünschen sortiert ist.

Dies ist buchstäblich auch mir 5 Minuten, um einzurichten und zu arbeiten.

Ich hatte eine vorhandene UITableViewVerwendung, CoreDatadie mit Daten aus iCloud gefüllt war und die ziemlich komplizierte Benutzerinteraktionen hat, und ich wollte nicht alles für eine replizieren müssen UISearchViewController. Ich konnte einfach ein Prädikat zu dem vorhandenen hinzufügen, das FetchRequestbereits von der verwendet wurde, FetchResultsControllerund das die bereits sortierten Daten filtert.

-(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];
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.