5.6Core Data II: Die Rückkehr der Objekte
Mit den in Abschnitt 5.3, »Core Data«, 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, Teile des Objektgraphen aus der Datenhaltung wieder in den Objektkontext zu laden. Mit der Methode executeFetchRequest:error: der Klasse NSManagedObjectContext 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 = @[theDescriptor];
return theFetch;
}
Listing 5.59 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 ermitteln es am einfachsten über eine Klassenmethode der Klasse NSEntityDescription.
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, so dass die Anfrage keine Suchbedingung braucht. Die Suche eines Textes über das Suchfeld verwendet hingegen ein Prädikat.
5.6.1Prä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 benutzen.
Dazu ein paar Beispiele:
// Prädikat aus dem vorherigen Beispiel:
[NSPrecicate predicateWithFormat:@"type = %@",
kMediumTypeImage]
// Falsch, primitive Datentypen sind mit %@ nicht erlaubt:
[NSPrecicate predicateWithFormat:@"age = %@", 5]
// Richtig, Zahl als NSNumber...
[NSPrecicate predicateWithFormat:@"age = %@", @5]
// ... oder mit %d
[NSPrecicate predicateWithFormat:@"age = %d", 5]
// Angabe des Attributnamens als Parameter
NSString *theName = @"type";
[NSPrecicate predicateWithFormat:@"%K = %@", theName,
kMediumTypeImage]
Listing 5.60 Beispiele für die Prädikaterzeugung
Innerhalb eines Prädikats können Sie die üblichen Vergleichsoperatoren (siehe Tabelle 5.2) verwenden, wobei Sie bei einigen Vergleichsarten mehrere Operatoren nutzen können; also ist beispielsweise age = 5 äquivalent zu age == 5.
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}"]
[NSPrecicate predicateWithFormat:@"age BETWEEN %@", @[@3, @5]]
Listing 5.61 Prädikate mit Bereichen
Für Zeichenketten gibt es spezielle Operatoren:
Operator | Vergleich |
Die Zeichenkette beginnt mit dem Wert auf der rechten Seite. |
|
Die Zeichenkette endet mit dem Wert auf der rechten Seite. |
|
Die Zeichenkette enthält den Wert auf der rechten Seite. |
|
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. |
|
Die Zeichenkette passt auf den regulären Ausdruck auf der rechten Seite. |
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 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 |
Mindestens ein Attribut muss die Bedingung erfüllen. |
|
Alle Attribute müssen die Bedingung erfüllen. |
|
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 hingegen 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 suchen Sie sehr einfach nach Objekten in Arrays, ohne eine Schleife schreiben zu müssen.
5.6.2Aktualisierung 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 entryAtIndexPath: implementieren Sie dann auf Basis dieses Arrays. Der Tableview für die Anzeige der Suchergebnisse verwendet auch genau dieses Vorgehen.
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. Dieses Verfahren ist zwar aufwendiger, hat jedoch den Vorteil, dass Sie die Änderungen im Tableview visualisieren können.
Core Data stellt Ihnen die Klasse NSFetchedResultsController zur Verfügung, die Ihnen diese Verbindung zwischen Suchanfragen und Tableviews vereinfacht. Außerdem unterstützt sie 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 keine Unterteilung in Abschnitte möchten. Wenn Sie einen Keypath 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 Fetched-Results-Controller innerhalb einer App verwenden, müssen Sie für jeden Controller einen anderen Namen für den Cache verwenden.
Die Klasse PhotoDiaryViewController verwendet einen Fetched-Results-Controller und hält ihn in der privaten Property fetchedResultsController. Der Viewcontroller initialisiert ihn in der Methode viewDidLoad und setzt sich selbst als Delegate des Fetched-Results-Controller. 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; die Änderungen des Nutzers am Tagebuch arbeitet er auf anderem Wege in das Suchergebnis ein.
NSFetchRequest *theRequest = self.fetchRequest;
NSFetchedResultsController *theController =
[[NSFetchedResultsController alloc] initWithFetchRequest:
theRequest managedObjectContext:self.managedObjectContext
sectionNameKeyPath:nil cacheName:@"Root"];
NSError *theError = nil;
theController.delegate = self;
if([theController performFetch:&theError]) {
self.fetchedResultsController = theController;
}
else {
NSLog(@"viewDidLoad: %@", theError);
}
Listing 5.62 Initialisierung des Fetched-Results-Controller
Nach dem Fetch können Sie auf alle gefundenen Objekte über die Property fetchedObjects zugreifen. Auf ein einzelnes Objekt im Ergebnis können Sie auch über die Methode objectAtIndexPath: zugreifen, wozu Sie die Indexpfad-Objekte verwenden können, die der Tableview an die Methoden der Datenquelle und des Delegates sendet. Die einzelnen Abschnitte liefert der Controller in einem Array über die Property sections, wobei jedes Element 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 deren Anzahl. Den eindeutigen Namen des Abschnitts fragen Sie über die Property name ab.
Mit dem Fetched-Results-Controller können Sie also 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.63 Datenquelle auf Basis eines Fetched-Results-Controllers
Diese Implementierung funktioniert sowohl für Tabellen mit als auch für solche ohne Segmentierung. Falls Sie Ihre Daten in Abschnitte unterteilen möchten, sollten Sie noch die Methode tableView:titleForHeaderInSection: der Datenquelle implementieren, was Sie ebenfalls über den Fetched-Results-Controller machen können:
- (NSInteger)tableView:(UITableView *)inTableView
titleForHeaderInSection:(NSInteger)inSection {
id<NSFetchedResultsSectionInfo> theInfo =
[[self.fetchedResultsController sections]
objectAtIndex:inSection];
return [theInfo name];
}
Listing 5.64 Implementierung des Abschnittstitels
Der Fetched-Results-Controller muss natürlich mitbekommen, wenn die App die Tagebucheinträge verändert. Das muss allerdings nicht unbedingt in dem Objektkontext des Fetched-Results-Controllers geschehen. Ebenso kann ja die App Objekte über einen anderen Objektkontext ä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 Photo-Diary-Viewcontroller 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.65 Registrierung für die Did-Save-Benachrichtigung
Über die Methode mergeChangesFromContextDidSaveNotification: können Sie die Änderungen, die ja das User-Info-Dictionary der Benachrichtigung enthält, direkt in den Objektkontext des Fetched-Results-Controllers 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.66 Import der Änderungen aus einer Benachrichtigung
Über die Benachrichtigungen bekommt nun der Fetched-Results-Controller alle Änderungen der App an den Daten mit; er wiederum muss die Änderungen an den Tableviewcontroller weitergeben.
5.6.3Das Delegate des Fetched-Results-Controllers

Der Viewcontroller ist deswegen das Delegate des Fetched-Results-Controllers und implementiert dessen Delegateprotokoll NSFetchedResultsControllerDelegate. Die Delegate-Methoden informieren direkt den Tableview über die Datenänderungen. Abbildung 5.25 stellt diesen Datenfluss schematisch dar.
Abbildung 5.25 Datenaktualisierung im Fototagebuch
Die Datenaktualisierung läuft wie folgt ab:
- Ein beliebiger Objektkontext speichert seine Daten. Dabei ruft er über eine Did-Save-Benachrichtigung an das Notificationcenter [Anm.: 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 Fetched-Results-Controller. Das geschieht automatisch, und Sie brauchen dafür nichts zu programmieren.
- Der Fetched-Results-Controller 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 Tableview-Viewcontrollers ä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 Fetched-Results-Controllers, 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 maximal vier Delegate-Methoden des Fetched-Results-Controllers. 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.67). 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.67 Beginn und Ende der Aktualisierung des Tableviews
Für die Veränderung jedes Objekts im Objektkontext ruft der Fetched-Results-Controller 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 Photodiary-Viewcontroller ü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:
@[inNewIndexPath]
withRowAnimation:UITableViewRowAnimationRight];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:
@[inIndexPath]
withRowAnimation:UITableViewRowAnimationRight];
break;
case NSFetchedResultsChangeMove:
[self.tableView deleteRowsAtIndexPaths:
@[inIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths:
@[inNewIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
theCell = [self.tableView
cellForRowAtIndexPath:inIndexPath];
[self applyDiaryEntry:inObject toCell:theCell];
break;
}
}
Listing 5.68 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. Diese Methode liefert Ihnen nur dann die entsprechende Zelle zurück, wenn der Tableview sie gerade anzeigt. Da der Viewcontroller die bereits beschriebene Methode applyDiaryEntry:toCell: (siehe Listing 5.52) für die Aktualisierung der Zellen verwendet, können Sie diese Methode auch hierfür einsetzen.
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, und 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 Fetched-Results-Controller-Delegates ist in den meisten Fällen sehr ähnlich. Sie können dafür in der Regel den Code des Photodiary-Viewcontrollers als Ausgangsbasis verwenden.
5.6.4Tabelleneinträ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 mit den Klassen UISearchBar und UISearchDisplayController sogar einen eigenen View samt Controller zur Verfügung. Sie können im Interface Builder diesen View 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.26, rechts). Bei dieser Aktion fügt der Interface Builder den View in den Tableview ein und legt einen Controller der Klasse UISearchDisplayController an. Außerdem verbindet er die Outlet-Property searchDisplayController der Klasse UIViewController mit dem neuen Controller, über die der Photodiary-Viewcontroller den Searchdisplaycontroller hält, und er stellt noch weitere Outlet-Verbindungen vom Searchdisplaycontroller zum Photodiary-Viewcontroller her.
Abbildung 5.26 Searchbar und Searchbar mit Controller
Der Searchdisplaycontroller 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 entryAtIndexPath: sieht beispielsweise folgendermaßen aus:
- (DiaryEntry *)entryAtIndexPath:(NSIndexPath *)inIndexPath {
if(self.searchDisplayController.isActive) {
return [self.searchResult
objectAtIndex:inIndexPath.row];
}
else {
return [self.fetchedResultsController
objectAtIndexPath:inIndexPath];
}
}
Listing 5.69 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.70 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 entfernen. Dazu müssen Sie den Code aus Listing 5.58 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.71 Löschen eines Eintrags aus dem Suchergebnis
In Listing 5.71 sind die neuen und geänderten Zeilen hervorgehoben. Die geänderte Methode erzeugt eine Kopie des Suchergebnisses über die Methode mutableCopy, und die damit erzeugte Kopie hat die Klasse NSMutableArray. Die Delegate-Methode entfernt das zu löschende Element aus dem Array und verwendet dann das geänderte Array 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.
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.