5.7 Core Data II: Die Rückkehr der Objekte
Mit den in Abschnitt 5.4 beschriebenen Methoden können Sie einen Objektgraphen erzeugen, verändern und sichern. Um die Daten im Tableview anzuzeigen, benötigen Sie noch eine Möglichkeit, um Teile des Objektgraphen aus der Datenhaltung wieder in den Objektkontext zu laden. Mit der Methode executeFetchRequest:error: können Sie Entitäten des gleichen Typs in der Datenhaltung suchen und laden.
Suchanfragen in Core Data beschreiben Sie über Objekte der Klasse NSFetchRequest. Eine Suchanfrage sucht immer nur die Objekte eines Entitätstyps. Dabei sucht Core Data auch nach Entitäten der Untertypen. Sie können dabei die Ergebnismenge natürlich durch eine Bedingung einschränken. Diese Prädikate können Sie über einen Objektbaum oder – was einfacher ist – über eine Zeichenkette formulieren. Sie erhalten das Ergebnis einer Suchanfrage in einem Array. Die Anordnung der Objekte im Array können Sie dabei in der Suchanfrage über eine Sortierreihenfolge bestimmen.
Sie erzeugen eine neue Suchanfrage, indem Sie ein Objekt der Klase NSFetchRequest anlegen und konfigurieren. Die Klasse PhotoDiaryViewController verwendet die folgende Suchanfrage, um die Tagebucheinträge im Tableview anzuzeigen:
- (NSFetchRequest *)fetchRequest {
NSFetchRequest *theFetch = [[NSFetchRequest alloc] init];
NSEntityDescription *theType = 
[NSEntityDescription entityForName:@"DiaryEntry"
inManagedObjectContext:self.managedObjectContext];
NSSortDescriptor *theDescriptor = [NSSortDescriptor
sortDescriptorWithKey:@"creationTime" ascending:NO];
theFetch.entity = theType;
theFetch.sortDescriptors =
[NSArray arrayWithObject:theDescriptor];
return theFetch;
}
Listing 5.57 Erzeugung einer Suchanfrage ohne Prädikat
Für die Konfiguration der Suchanfrage benötigen Sie den Entitätstyp, den ein Objekt der Klasse NSEntityDescription beschreibt. Es ist im Datenmodell enthalten, und Sie können es am einfachsten über eine Klassenmethode der Klasse NSEntityDescription ermitteln.
Die Sortierreihenfolge beschreiben Sie durch ein Array mit Objekten der Klasse NSSortDescriptor. Diese enthalten jeweils den Namen des zu sortierenden Attributs und einen booleschen Wert für die Sortierrichtung. Dabei steht YES für auf- und NO für absteigend.
Über die Methode setPredicate: können Sie eine Suchbedingung in der Anfrage setzen und nur nach Einträgen suchen, die darauf passen. Der Tableview auf der Startseite des Fototagebuchs zeigt immer alle Einträge an, sodass die Anfrage keine Suchbedingung braucht. Die Suche eines Textes über das Suchfeld verwendet hingegen ein Prädikat.
5.7.1 Prädikate

Ein Prädikat beschreibt eine Bedingung, die Sie auf Objekte anwenden können. Die Auswertung liefert einen booleschen Wert, der angibt, ob die Bedingung für das Objekt wahr oder falsch ist. Am einfachsten formulieren Sie ein Prädikat über eine Zeichenkette. Dazu besitzt die Klasse NSPredicate den Convenience-Konstruktor predicateWithFormat:. Sie können beispielsweise alle Bilder mit dem Prädikat
[NSPrecicate predicateWithFormat:@"type = 'image'"]
aus den Medien herausfiltern. Zeichenketten müssen Sie wie im Beispiel durch einfache oder doppelte Anführungszeichen maskieren.
In den meisten Fällen sind Prädikate jedoch nicht statisch, sondern besitzen Parameter. Sie können in der Zeichenkette – ähnlich wie beim NSString-Konstruktor stringWithFormat: – Platzhalter verwenden:
- Der Platzhalter %@ steht für ein beliebiges Objekt. Sie können hierfür also Zahlen, Zeichenketten, Datumswerte usw. einsetzen. Sie dürfen allerdings keine primitiven Typen wie int, float oder double verwenden. Diese Werte müssen Sie erst in ein NSNumber-Objekt verpacken.
- Alternativ können Sie für die primitiven Datentypen die bekannten Platzhalter %d, %f, %u usw. verwenden, die Sie von stringWithFormat: oder NSLog kennen.
- Wenn Sie Attributnamen oder -pfade als Parameter in dem Prädikat verwenden möchten, müssen Sie den Platzhalter %K verwenden.
Dazu ein paar Beispiele:
// Prädikat aus dem vorherigen Beispiel:
[NSPrecicate predicateWithFormat:@"type = %@", theType]
// Falsch, primitive Datentypen sind mit %@ nicht erlaubt:
[NSPrecicate predicateWithFormat:@"age = %@", 5]
// Richtig, Zahl als NSNumber...
NSNumber *theAge = [NSNumber numberWithInt:5];
[NSPrecicate predicateWithFormat:@"age = %@", theAge]
// ... oder mit %d
[NSPrecicate predicateWithFormat:@"age = %d", 5]
// Angabe des Attributnamens als Parameter
NSString *theAttributeName = @"type";
[NSPrecicate predicateWithFormat:@"%K = %@", theAttributeName
theType]
Listing 5.58 Beispiele für die Prädikaterzeugung
Innerhalb eines Prädikats können Sie die üblichen Vergleichsoperatoren (siehe Tabelle 5.3) verwenden.
| Operator(en) | Vergleich | Operator(en) | Vergleich |
|
=, == |
Gleich |
!=, <> |
Ungleich |
|
< |
Kleiner |
<= |
Kleiner gleich |
|
> |
Größer |
>= |
Größer gleich |
Mit dem Operator BETWEEN können Sie prüfen, ob ein Wert in einem Bereich liegt. Wenn Sie den Bereich als Parameter angeben möchten, müssen Sie dafür ein Array verwenden:
[NSPrecicate predicateWithFormat:@"age BETWEEN {3, 5}"]
NSArray *theLimits = [NSArray arrayWithInt:[NSNumber
numberWithInt:3], [NSNumber numberWithInt:5], nil];
[NSPrecicate predicateWithFormat:@"age BETWEEN %@",
theLimits]
Listing 5.59 Prädikate mit Bereichen
Für Zeichenketten gibt es spezielle Operatoren:
| Operator | Vergleich |
|
BEGINSWITH |
Die Zeichenkette beginnt mit dem Wert auf der rechten Seite. |
|
ENDSWITH |
Die Zeichenkette endet mit dem Wert auf der rechten Seite. |
|
CONTAINS |
Die Zeichenkette enthält den Wert auf der rechten Seite. |
|
LIKE |
Die Zeichenkette stimmt mit dem Wert auf der rechten Seite überein, der die Wildcards ? und * enthalten darf. Dabei steht ? für genau ein Zeichen und * für beliebig viele Zeichen. |
|
MATCHES |
Die Zeichenkette passt auf den regulären Ausdruck auf der |
Diese Operatoren unterscheiden zwischen Groß- und Kleinschreibung. Sie können an die Operatoren [c] oder [cd] anfügen, um Vergleiche unabhängig von der Schreibweise zu machen. Mit dem Suffix [c] gilt das allerdings nur für die 26 Buchstaben des Alphabets, während [cd] auch die diakritischen Zeichen (Umlaute, Buchstaben mit Akzent) einschließt. Beispielsweise prüft text CONTAINS ’all’, ob das Attribut text die Zeichenkette »all« enthält. Dieses Prädikat ist für die Zeichenketten »Kalle« und »Weltall« wahr, jedoch für »Alle Vögel« falsch. Um die letzte Zeichenkette einzuschließen, müssen Sie das Prädikat so erzeugen: text CONTAINS[cd] ’all’.
Tipp
Verwenden Sie im Deutschen immer das Suffix [cd], wenn Sie unabhängig von der Schreibweise vergleichen wollen.
Sie können Vergleiche durch die binären booleschen Operatoren AND und OR miteinander verknüpfen, durch den unären Operator NOT negieren und durch runde Klammern gruppieren.
text CONTAINS[cd] ’all’ AND NOT (age = 3 OR age = 7)
Sie können mit den Prädikaten jedoch nicht nur die Attribute eines Objekts überprüfen, sondern auch die Attribute der über Relationships verbundenen Objekte mit einbeziehen. Bei To-One-Relationships schreiben Sie einfach den Relationship-Pfad in die Bedingung. Wenn Sie beispielsweise alle Medien suchen wollen, deren Tagebucheintrag vor einem bestimmten Datum angelegt wurde, dann können Sie das Prädikat so formulieren: diaryEntry.creationTime < %@.
To-Many-Relationships verweisen in der Regel auf mehrere Attributwerte. Im Prädikat müssen Sie deswegen durch einen Operator angeben, wie die Bedingung auf die Attribute zutreffen soll.
| Operator | Bedeutung |
|
ANY, SOME |
Mindestens ein Attribut muss die Bedingung erfüllen. |
|
ALL |
Alle Attribute müssen die Bedingung erfüllen. |
|
NONE |
Kein Attribut darf die Bedingung erfüllen. |
Sie können über ANY media.type = 'image' alle Tagebucheinträge suchen, die mindestens ein Bildmedium haben. Über das Prädikat ALL media.type = 'image' finden Sie hingegen nur Einträge, bei denen alle Medien Bilder sind. Um nur Einträge ohne Bildmedien zu finden, können Sie das Prädikat NONE media.type = 'image' verwenden.
Wie Sie ein Prädikat in einer Core-Data-Suche verwenden, haben Sie bereits erfahren. Sie weisen es einfach über die Methode setPredicate: dem FetchRequest zu. Sie können Prädikate auch über die Methode filteredArrayUsingPredicate: auf Arrays anwenden. Damit können Sie sehr einfach nach Objekten in Arrays suchen, ohne eine Schleife schreiben zu müssen.
5.7.2 Aktualisierung des Tableviews

Sie haben jetzt die wichtigsten Bausteine, um die Einträge des Fototagebuchs in einem Tableview anzeigen zu lassen. Sie können die Datenquelle über eine Suchanfrage und ein Array implementieren. Über den Objektkontext und die Suchanfrage laden Sie die Objekte in ein Array, das der Controller als Property hält. Die Methoden tableView:numberOfRowsInSection: und entryForTableView: atIndexPath: implementieren Sie dann auf Basis dieses Arrays. Der Tableview für die Anzeige der Suchergebnisse verwendet dieses Vorgehen – aber dazu später mehr.
Bei diesem Vorgehen können Sie den Tableview einfach neu laden, um die Anzeige nach dem Löschen oder Hinzufügen von Einträgen zu aktualisieren. Das ist der einfachste Weg. Dieses Vorgehen gibt dem Nutzer jedoch leider keine optische Rückmeldung über seine letzte Aktion.
Alternativ können Sie die Aktualisierungen auch im Array und im Tableview ausführen. Dann müssen Sie beispielsweise nach dem Hinzufügen eines Eintrags über Core Data auch diesen Eintrag zu Ihrem Array und dem Tableview hinzufügen. Dabei müssen Sie den neuen Eintrag an der gleichen Position im Tableview und im Array einfügen, die der Eintrag auch im entsprechenden Suchergebnis hat.
Core Data stellt Ihnen die Klasse NSFetchedResultsController zur Verfügung, die Ihnen diese Verbindung zwischen Suchanfragen und Tableviews vereinfacht. Außerdem unterstützt sie auch die Unterteilung der Daten in Abschnitte anhand eines Attributs.
Sie erzeugen einen neuen Controller über die Initializer-Methode initWithFetchRequest:managedObjectContext:sectionNameKeyPath:cacheName:. Dabei müssen Sie eine Suchanfrage und einen Objektkontext angeben. Für die Parameter für den Keypath des Abschnittsnamens und für den Cachenamen dürfen Sie nil verwenden. Wenn Sie einen Key-Path angeben, unterteilt der Controller die Einträge in Abschnitte. Dabei ermittelt er für jeden Eintrag über den Keypath den Namen des Abschnitts, zu dem der Eintrag gehört.
Der Controller kann seine Ergebnisse in einem Cache speichern, wenn Sie einen Cachenamen angeben. Falls Sie mehrere Fetchedresultscontroller innerhalb einer App verwenden, müssen Sie für jeden Cache einen anderen Namen verwenden.
Die Klasse PhotoDiaryViewController verwendet einen Fetchedresultscontroller und hält ihn in der privaten Property fetchedResultsController. Der Viewcontroller initialisiert ihn in der Methode viewDidLoad und setzt sich selbst als dessen Delegate. Anschließend füllt sie den Controller über einen Aufruf der Methode performFetch:. Das ist die einzige Stelle in der Klasse, an der der Viewcontroller einen Fetch ausführt.
Nach dem Fetch können Sie auf alle gefundenen Objekte über die Property fetchedObjects zugreifen. Auf ein einzelnes Objekt im Ergebnis können Sie über die Methode objectAtIndexPath: zugreifen, wozu Sie die Indexpath-Objekte Ihres Tableviews verwenden können. Die einzelnen Abschnitte liefert der Controller über die Property sections, wobei jedes Element in dem Array das Protokoll NSFetchedResultsSectionInfo implementiert.
Das Protokoll deklariert Propertys, mit denen Sie die Daten des Abschnitts erhalten. Die Property objects liefert die Einträge des Abschnitts, und numberOfObjects liefert deren Anzahl. Den Titel des Abschnitts können Sie über die Property name abfragen. Mit dem Fetchedresultscontroller können Sie die Datenquelle eines Tableviews recht einfach implementieren, wenn Sie den Controller in einer Property halten:
- (NSInteger)numberOfSectionsInTableView:
(UITableView *)inTableView {
return [[self.fetchedResultsController sections] count];
}
- (NSInteger)tableView:(UITableView *)inTableView
numberOfRowsInSection:(NSInteger)inSection {
id<NSFetchedResultsSectionInfo> theInfo =
[[self.fetchedResultsController sections]
objectAtIndex:inSection];
return [theInfo numberOfObjects];
}
- (UITableViewCell *)tableView:(UITableView *)inTableView
cellForRowAtIndexPath:(NSIndexPath *)inIndexPath {
DiaryEntry *theEntry = [self.fetchedResultsController
objectAtIndexPath:inIndexPath];
...
}
Listing 5.60 Datenquelle mit einem Fetchedresultscontroller
Diese Implementierung funktioniert sowohl für Tabellen mit als auch ohne Segmentierung. Falls Sie Ihre Daten in Abschnitte unterteilen möchten, sollten Sie noch die Methode tableView:titleForHeaderInSection: der Datenquelle implementieren, was Sie auch über den Controller machen können:
- (NSInteger)tableView:(UITableView *)inTableView
titleForHeaderInSection:(NSInteger)inSection {
id<NSFetchedResultsSectionInfo> theInfo =
[[self.fetchedResultsController sections]
objectAtIndex:inSection];
return [theInfo name];
}
Listing 5.61 Implementierung des Abschnittstitels
Der Fetchedresultscontroller muss natürlich mitbekommen, wenn die App die Tagebucheinträge verändert. Das muss allerdings nicht unbedingt in dem Objektkontext des Fetchedresultscontrollers geschehen. Ebenso kann ja die App Objekte über einen anderen Objektcontroller ändern. Der Controller muss also immer seine Daten aktualisieren, wenn irgendein Objektkontext seine Objekte sichert.
Dazu verwendet die App die Did-Save-Benachrichtigungen der Klasse NSManagedObjectContext. Der Photodiaryviewcontroller registriert nach dem Laden seines Views die Methode managedObjectContextDidSave: im Notificationcenter für diesen Benachrichtigungstyp.
NSNotificationCenter *theCenter =
[NSNotificationCenter defaultCenter];
[theCenter addObserver:self
selector:@selector(managedObjectContextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:nil];
Listing 5.62 Registrierung für die Did-Save-Benachrichtigung
Über die Methode mergeChangesFromContextDidSaveNotification: können Sie die Änderungen, die ja das Userinfo-Dictionary der Benachrichtigung enthält, direkt in den Objektkontext des Fetchedresultscontrollers importieren. Das sollten Sie natürlich nur machen, wenn die Benachrichtigung nicht von diesem Controller stammt.
- (void)managedObjectContextDidSave:
(NSNotification *)inNotification {
if(inNotification.object != self.managedObjectContext) {
[self.managedObjectContext
mergeChangesFromContextDidSaveNotification:
inNotification];
}
}
Listing 5.63 Import der Änderungen aus einer Benachrichtigung
5.7.3 Das Delegate des Fetchedresultscontrollers

Der Fetchedresultscontroller wiederum muss die Änderungen an den Tableviewcontroller weitergeben. Der Viewcontroller ist deswegen das Delegate des Fetchedresultscontrollers und implementiert dessen Delegate-Protokoll NSFetchedResultsControllerDelegate. Die Delegate-Methoden informieren direkt den Tableview über die Datenänderungen. Abbildung 5.33 stellt den Datenfluss schematisch dar.
Die Datenaktualisierung läuft wie folgt ab:
- Ein beliebiger Objektkontext speichert seine Daten. Dabei ruft er über eine DidSave-Benachrichtigung an das Notificationcenter [Das Notificationcenter ist in der Abbildung nicht dargestellt.] die Methode managedObjectContextDidSave: des Viewcontrollers auf.
- Die Methode aktualisiert den Objektkontext des Viewcontrollers, indem sie die Änderungen aus der Benachrichtigung in den Kontext einfließen lässt.
- Dieser Objektkontext benachrichtigt den Fetchedresultscontroller. Das geschieht automatisch, und Sie brauchen dafür nichts zu programmieren.
- Der Fetchedresultscontroller ruft seine Delegate-Methoden auf. Dadurch leitet er die Änderungen wieder an den Viewcontroller weiter.
- Die Delegate-Methoden aktualisieren den Tableview.
Der ganze Ablauf ist recht komplex und wirkt unnötig kompliziert. Er hat allerdings auch gravierende Vorteile. Die Objektkontexte, die den Aktualisierungsvorgang auslösen, sind vollkommen unabhängig von dem Viewcontroller. Sie können also zu der App beliebige weitere Objektkontexte hinzufügen, ohne die Logik des Photodiaryviewcontrollers ändern zu müssen. Jedes Mal, wenn einer dieser Objektkontexte seine Elemente speichert, aktualisiert er auch automatisch den Tableview.
Ein weiterer Vorteil entsteht durch die Nutzung des Fetchedresultscontrollers, der Ihnen eine relativ einfache Implementierung der Datenquelle erlaubt. Außerdem wertet er die Änderungen an den Core-Data-Objekten aus und berechnet die Indexpfade für die Aktualisierung des Tableviews.
Diese Aktualisierungen erfolgen über vier Delegate-Methoden des Fetchedresultscontrollers. Der Controller klammert alle zusammenhängenden Änderungen zwischen Aufrufe der Delegate-Methoden controllerWillChangeContent: und controllerDidChangeContent:. Über diese Methoden können Sie den Tableview über bevorstehende Änderungen und deren Abschluss informieren (siehe Listing 5.64). Durch diese Klammerung führt der Tableview die dazwischenliegenden Aktualisierungsoperationen simultan in einem Block aus.
- (void)controllerWillChangeContent:
(NSFetchedResultsController *)inController {
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:
(NSFetchedResultsController *)inController {
[self.tableView endUpdates];
}
Listing 5.64 Beginn und Ende der Aktualisierung des Tableviews
Für die Veränderung jedes Objekts im Objektkontext ruft der Fetchedresultscontroller jeweils einmal die Delegate-Methode controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: auf. Dabei enthält der zweite Parameter das geänderte Objekt. Der dritte und der fünfte Parameter enthalten die Indexpfade mit der bisherigen beziehungsweise der neuen Position des Objekts in der Tabelle. Der Wert des vierten Parameters beschreibt die Änderungsart:
Diese Änderungen setzt der Photodiaryviewcontroller über eine Fallunterscheidung anhand des Typs in Aktualisierungsoperationen auf dem Tableview um.
- (void)controller:(NSFetchedResultsController *)inController
didChangeObject:(id)inObject
atIndexPath:(NSIndexPath *)inIndexPath
forChangeType:(NSFetchedResultsChangeType)inType
newIndexPath:(NSIndexPath *)inNewIndexPath {
id theCell;
switch(inType) {
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:
[NSArray arrayWithObject:inNewIndexPath]
withRowAnimation:UITableViewRowAnimationRight];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:
[NSArray arrayWithObject:inIndexPath]
withRowAnimation:UITableViewRowAnimationRight];
break;
case NSFetchedResultsChangeMove:
[self.tableView deleteRowsAtIndexPaths:
[NSArray arrayWithObject:inIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths:
[NSArray arrayWithObject:inNewIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
theCell = [self.tableView
cellForRowAtIndexPath:inIndexPath];
[self applyDiaryEntry:inObject toCell:theCell];
break;
}
}
Listing 5.65 Aktualisierung des Tableviews
Während Sie das Einfügen und Löschen durch die jeweils analogen Methoden insertRowsAtIndexPaths:withRowAnimation: beziehungsweise deleteRowsAtIndexPaths:withRowAnimation: umsetzen können, müssen Sie das Verschieben durch ein Löschen und ein Einfügen umsetzen. Das Fototagebuch benötigt diese Änderungsart zwar nicht, weil es die Einträge immer in der Erzeugungsreihenfolge anzeigt. Wenn Sie jedoch ein anderes Sortierkriterium verwenden oder die Erzeugungszeit editierbar machen, müssen Sie auch diesen Fall implementieren.
Für den Änderungstyp NSFetchedResultsChangeUpdate müssen Sie nicht den Tableview, sondern den Inhalt einer Zelle verändern. Sie können sich dazu die Zelle über die Methode cellForRowAtIndexPath: des Tableviews holen. Da der Viewcontroller die bereits beschriebene Methode applyDiaryEntry:toCell: (siehe Listing 5.51) für die Aktualisierung der Zellen verwendet, können Sie diese Methode hier auch aufrufen.
Wenn Sie Ihren Tableview in Abschnitte unterteilt haben, sollten Sie auch die Methode controller:didChangeSection:atIndex:forChangeType: implementieren. Sie ist das Gegenstück zu der gerade besprochenen Delegate-Methode für Objekte. Der Parameter für den Änderungstyp kann hier nur die Werte für Einfügen oder Löschen annehmen. Die Implementierung dieser beiden Fälle erfolgt analog über die Methoden insertSections:withRowAnimation: beziehungsweise deleteSections:withRowAnimation: des Tableviews.
Tipp
Die Implementierung des Fetchedresultscontroller-Delegates ist in den meisten Fällen sehr ähnlich. Sie können dafür in der Regel den Code des Photodiaryviewcontrollers als Ausgangsbasis verwenden.
5.7.4 Tabelleneinträge suchen
Der Tableview zeigt immer das komplette Tagebuch an. Wenn Sie Ihr Tagebuch ständig erweitern, haben Sie schnell sehr viele Einträge darin. Dann kann es für Sie schwierig werden, bestimmte Einträge wiederzufinden. Bei Tableviews mit vielen Einträgen sollten Sie dem Nutzer eine Möglichkeit geben, gezielt nach Einträgen zu suchen.
Dafür stellt Cocoa Touch sogar einen eigenen View samt Controller zur Verfügung. Sie können im Interface Builder einen View der Klasse UISearchBar direkt auf einen Tableview ziehen. Wenn Sie allerdings den View mit einem Searchbarcontroller verwenden möchten, können Sie auch direkt beide Komponenten in einem Schritt anlegen. Ziehen Sie dazu das Element Search Bar with Search Display Controller auf den Tableview (siehe Abbildung 5.34 rechts). Bei dieser Aktion fügt der Interface Builder den View in den Tableview ein und legt einen Controller der Klasse UISearchDisplayController an. Sie brauchen dann nur noch eine Outlet-Property anzulegen, über die der Photodiaryviewcontroller den Searchdisplaycontroller hält. Außerdem sollte der Viewcontroller als Delegate des Searchdisplaycontrollers dienen.
Abbildung 5.34 Searchbar und Searchbar mit Controller
Dieser Controller besitzt einen eigenen Tableview für die Anzeige des Suchergebnisses, der die gleiche Datenquelle und das gleiche Delegate wie der Tableview des Viewcontrollers verwendet. Aus diesem Grund müssen Sie an einigen Stellen unterscheiden, für welchen Tableview die Methode der Datenquelle beziehungsweise des Delegates aufgerufen wurde. Der Viewcontroller macht das über einen Vergleich mit seiner Property tableView. Die Methode entryForTableView:atIndexPath: sieht beispielsweise folgendermaßen aus:
- (DiaryEntry *)entryForTableView:(UITableView *)inTableView
atIndexPath:(NSIndexPath *)inIndexPath {
if(inTableView == self.tableView) {
return [self.fetchedResultsController
objectAtIndexPath:inIndexPath];
}
else {
return [self.searchResult
objectAtIndex:inIndexPath.row];
}
}
Listing 5.66 Bestimmung des Tagebucheintrags zu einem Indexpfad
Das Suchergebnis speichert der Viewcontroller in der Property searchResult. Dieses Array setzt die Delegate-Methode searchDisplayController:shouldReloadTableForSearchString:, die für die Berechnung des Suchergebnisses vorgesehen ist. Die Methode liefert einen booleschen Wert zurück, über den Sie das Neuladen des Tableviews für die Suche steuern können.
- (BOOL)searchDisplayController:
(UISearchDisplayController *)inController
shouldReloadTableForSearchString:(NSString *)inValue {
NSFetchRequest *theRequest = self.fetchRequest;
NSPredicate *thePredicate =
[NSPredicatepredicateWithFormat:
@"text contains[cd] %@", inSearchString];
theRequest.predicate = thePredicate;
theRequest.fetchLimit = 30;
self.searchResult = [self.managedObjectContext
executeFetchRequest:theRequest error:NULL];
return YES;
}
Listing 5.67 Berechnung des Suchergebnisses
Für die Berechnung des Suchergebnisses verwendet die Methode ein Prädikat, mit dem sie eine Suche über die Methode executeFetchRequest:error: durchführt. Der Searchdisplaycontroller ruft die Delegate-Methode auf, sobald der Nutzer den ersten Buchstaben eingegeben hat. Bei kurzen Eingaben kann es natürlich passieren, dass das Prädikat auf alle Tagebucheinträge passt. Sie sollten also Vorkehrungen treffen, dass der Fetchrequest in diesem Fall nicht alle Einträge lädt und die App wegen Speichermangels abstürzt. Aus diesem Grund begrenzt die Delegate-Methode die maximale Anzahl der Ergebniselemente über den Setter fetchLimit auf 30.
Der Tableview des Searchdisplaycontrollers besitzt die gleichen Eigenschaften wie der des Viewcontrollers. Sie können über die Suche auch Einträge löschen. Sie müssen also beim Löschen den Eintrag auch aus dem Suchergebnis löschen. Dazu müssen Sie den Code aus Listing 5.56 anpassen:
- (UITableView *)searchResultsTableView {
return self.searchDisplayController.searchResultsTableView;
}
- (void)tableView:(UITableView *)inTableView
commitEditingStyle:(UITableViewCellEditingStyle)inStyle
forRowAtIndexPath:(NSIndexPath *)inIndexPath {
if(inStyle == UITableViewCellEditingStyleDelete) {
DiaryEntry *theItem = [self entryForTableView:
inTableView atIndexPath:inIndexPath];
NSError *theError = nil;
[self.managedObjectContext rollback];
[self.managedObjectContext deleteObject:theItem];
if([self.managedObjectContext save:&theError]) {
if(inTableView == self.searchResultsTableView) {
NSMutableArray *theResult =
[self.searchResult mutableCopy];
[theResult removeObjectAtIndex:inIndexPath.row];
self.searchResult = theResult;
[inTableView deleteRowsAtIndexPaths:
[NSArray arrayWithObject:inIndexPath]
withRowAnimation:
UITableViewRowAnimationFade];
}
}
else {
NSLog(@"Unresolved error %@", theError);
}
}
}
Listing 5.68 Löschen eines Eintrags aus dem Suchergebnis
In Listing 5.68 sind die neuen und geänderten Zeilen hervorgehoben. Die geänderte Methode erzeugt eine Kopie des Suchergebnisses über die Methode mutableCopy. Die damit erzeugte Kopie hat die Klasse NSMutableArray. Wie der Name schon andeutet, können Sie Arrays dieser Klasse auch nach der Konstruktion verändern. Die Delegate-Methode entfernt das zu löschende Element aus dem Array und setzt dieses dann als neues Suchergebnis. Danach aktualisiert sie den Tableview für das Suchergebnis über die Methode deleteRowsAtIndexPaths:withRowAnimation:.
Hinweise zur Speicherverwaltung
Die Property searchResult hat den Speicherverwaltungstyp copy. Die Zuweisung erzeugt also erst eine Kopie des Arrays theResult, weil es ja die Klasse NSMutableArray hat. Die Property zeigt also immer auf ein unveränderliches Array.
Ihr Kommentar
Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.






Jetzt bestellen





