5.5Tableviews und Core Data
Sie haben jetzt ein Grundgerüst für die Erstellung neuer und die Aktualisierung bestehender Tagebucheinträge. Sie brauchen im Tagebuch noch eine Möglichkeit, sich alle Einträge anzeigen zu lassen. Dazu ist ein Tableview am besten geeignet. Er zeigt alle Einträge des Fototagebuchs an.
5.5.1Tableviews

Tableviews eignen sich besonders für die Anzeige großer Mengen gleichartiger Daten, wobei Sie die Daten in Abschnitte unterteilen können. Tableviews sind ein gutes Beispiel dafür, wie Sie das Aussehen und Verhalten eines Views verändern können, ohne von der Viewklasse ableiten zu müssen. Jeder Tableview besitzt ein Delegate und eine Datenquelle, deren Funktionsweise einem Delegate sehr stark ähnelt. Der View, das Delegate und die Datenquelle teilen ihre Zuständigkeiten folgendermaßen auf:
- Die Klasse UITableView übernimmt den Aufbau, die Darstellung und die Gestensteuerung der Tabelle. Die Tabelle besteht aus einer Spalte, die beliebig viele Zellen enthalten kann. Der Tableview kann die Zellen zu Abschnitten zusammenfassen.
- Die Datenquelle implementiert das Protokoll UITableViewDataSource und liefert die Anzahl der Abschnitte sowie die Anzahl der Zeilen pro Abschnitt. Sie ist außerdem dafür zuständig, die Views für die Zellen der Tabelle zu erzeugen und zu konfigurieren.
- Das Delegate implementiert das Protokoll UITableViewDelegate und stellt Methoden für die Ereignisverarbeitung des Tableviews bereit. Beispielsweise können Sie sich damit über die Auswahl von Zellen informieren lassen.
Leider ist die Unterscheidung zwischen der Datenquelle und dem Delegate nicht ganz so strikt. So besitzt zum Beispiel das Delegate Methoden, mit denen Sie die Höhe der Zellen oder das Aussehen der Abschnittsheader und -footer festlegen können. Sowohl die Datenquelle als auch das Delegate implementieren Sie in der Regel in dem Viewcontroller, der den Tableview verwaltet. Generell sind ja Viewcontroller ein guter Platz, um die Delegates der Views zu implementieren.
Mit der Klasse UITableViewController stellt das UIKit eine Basis für Tabellenansichten bereit. Die Klasse implementiert bereits die beiden Protokolle und erzeugt automatisch einen Tableview, so dass Sie keinen mehr im Storyboard anlegen müssen. Sie können im Tableview auch Prototypen für die Tabellenzellen anlegen. Dieser Mechanismus ähnelt der Verwaltung der Zellen in statischen Tableviews, die Sie bereits in Kapitel 4, »Alles unter Kontrolle«, kennengelernt haben.
Wenn Sie eine Datenquelle für einen Tableview anlegen, müssen Sie in der Regel drei Methoden überschreiben. Mit der Methode numberOfSectionsInTableView: legen Sie fest, in wie viele Abschnitte Sie Ihre Zellen unterteilen wollen. Die Anzahl der Zellen innerhalb eines Abschnitts erfragt der Tableview über die Methode tableView:numberOfRowsInSection:.
Tableviews sind übrigens hinsichtlich des Speichers optimiert. Im besten Fall brauchen sie nur ungefähr so viele Zellen, wie sie auch tatsächlich auf einmal auf dem Bildschirm darstellen. Außerdem zerstört der Tableview aus der Ansicht gescrollte Zellen nicht, sondern merkt sie sich für eine spätere Wiederverwendung. Durch diesen nachhaltigen Umgang mit Zellen und deren Daten können Tableviews auch Datenmengen anzeigen, deren Speicherbedarf wesentlich größer als der verfügbare Hauptspeicher des Geräts ist. Das ist natürlich nur möglich, wenn der Tableview immer nur einzelne Zellen von der Datenquelle anfordert. Dazu dient die Methode tableView:cellForRowAtIndexPath:.
Der zweite Parameter dieser Methode hat die Klasse NSIndexPath und enthält die Position einer Zelle oder eines Elements in der Tabelle. Auf diese Werte greifen Sie über die Propertys section und row zu.
Sie können über die Methode dequeueReusableCellWithIdentifier: oder dequeueReusableCellWithIdentifier:forIndexPath: beim Tableview eine neue Zelle anfordern. Der Tableview liefert dann entweder eine vorhandene, jedoch nicht verwendete Zelle oder er erstellt aus dem Prototyp mit der angegebenen Kennung eine neue Zelle. Falls Sie keinen entsprechenden Prototyp registriert haben, liefert die Methode nil, und Sie müssen die Zelle im Code erzeugen. Allerdings gibt es drei unterschiedliche Möglichkeiten, Prototypen zu registrieren, so dass eine Erzeugung der Zellen im Programmcode in der Regel nicht notwendig ist.
Die Zellen müssen übrigens nicht alle gleich aussehen. Die verschiedenen Arten von Zellen unterscheidet der Tableview anhand frei wählbarer Kennungen, die Sie als Parameter »Identifier« an die Dequeue-Methoden übergeben. Dabei haben Zellen der gleichen Art immer die gleiche Kennung, während unterschiedliche Zellarten natürlich unterschiedliche Kennungen verwenden sollten.
Projektinformation
Den Quellcode zu dem folgenden Beispiel finden Sie auf der DVD unter Code/Apps/iOS/TableView oder im Github-Repository zum Buch im Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Auflage_3/Apps/iOS5/TableView.
Das Beispielprojekt TableView demonstriert die Verwendung einer Tabelle mit unterschiedlichen Zellarten. Die Zellarten unterscheiden sich allerdings dabei nur durch unterschiedliche Textfarben.
Unterschiedliche Zellarten
Eine Unterteilung der Zellarten nach der Textfarbe ist in der Regel allerdings wenig sinnvoll. Nutzen Sie unterschiedliche Zellarten, wenn die Zellen einen unterschiedlichen Aufbau haben. Wenn Sie zum Beispiel einen Produktkatalog über einen Tableview darstellen möchten, können Sie einen Zelltyp für gewöhnliche Produkte und einen für besondere Angebote vorsehen. Der zweite Typ könnte beispielsweise das Produktbild größer darstellen und den Preis in einem Stern anzeigen.
Die Zellen enthalten außerdem einen Zähler, der ihre Erzeugungsreihenfolge angibt. Durch Scrollen können Sie feststellen, dass gleiche Zähler in unterschiedlichen Zellen auftreten können beziehungsweise dass die Zellen des gleichen Elements unterschiedliche Zähler haben können. Der Tableview verwendet die Zellen also mehrfach für unterschiedliche Elemente. Sie können an diesem Beispiel also sehen, wie der Tableview die Zellen wiederverwendet.
5.5.2Tabellenzellen gestalten

Für viele Anwendungsfälle reichen die Darstellungsmöglichkeiten der Klasse UITableViewCell allerdings nicht aus. Die Standardzellen haben ein relativ starres Aussehen, auf das Sie wenig Einflussmöglichkeiten haben. Beispielsweise können diese Zellen zwar ein Bild darstellen; Sie können hingegen weder dessen Position noch dessen Größe festlegen.
Über die Property contentView erlaubt die Klasse UITableViewCell jedoch auch die Anzeige beliebiger Elemente in freier Anordnung. Natürlich können Sie diese Views durch Programmanweisungen erzeugen. Es ist allerdings auch möglich, dafür den Interface Builder zu verwenden. In der Elementbibliothek finden Sie dafür einen View der Klasse UITableViewCell, den Sie als Basis für Ihre eigenen Zellen verwenden können. Sie sollten auch jeder Zelle, die Sie im Interface Builder erstellen, eine eindeutige Kennung über ihren Attributinspektor geben. Die Zelle können Sie dann wie jeden anderen View auch konfigurieren und mit Subviews füllen. Der Interface Builder legt die Subviews allerdings immer im Contentview der Zelle ab.
Hinweis
Legen Sie Ihre Views ausschließlich in den Contentview und niemals als direkte Subviews der Zelle ab. Sie sollten Ihre Views auch niemals in die anderen Views der Zelle einfügen. Wenn Sie den Contentview der Zelle verwenden, sollten Sie nicht auf deren Standard-Views (z. B. Titel, Icon) zugreifen. Dadurch kann es zu Darstellungsfehlern kommen, da die Standard-Views Ihre Views überlagern.
Die Klasse UITableView bietet drei Möglichkeiten, Prototypen für Zellen zu registrieren.
5.5.3Zellprototypen über das Storyboard definieren

Wenn Sie den Viewcontroller über ein Storyboard verwalten, können Sie die Zellprototypen direkt in den Tableviewcontroller ziehen (siehe Abbildung 5.22), wie Sie es bereits bei dem statischen Tableview im vierten Kapitel kennengelernt haben. Da das Fototagebuch hingegen die Zellen dynamisch erzeugen soll, müssen Sie im Attributinspektor des Tableviews unter Content den Eintrag Dynamic Prototypes auswählen. Den Inhalt der Zelle können Sie dann beliebig gestalten.
Kennung der Zellprototypen
Denken Sie daran, jedem Zellprototypen über seinen Attributinspektor eine eigene, eindeutige Kennung zuzuweisen. Sie brauchen diese Kennung, um über die Methoden dequeueReusableCellWithIdentifier: beziehungsweise dequeueReusableCellWithIdentifier:forIndexPath: neue Zellen erzeugen zu können.
Abbildung 5.22 Tabellenzellen im Storyboard
Tabellenzellen haben eine Standardhöhe von 44 Punkten; andere Höhen müssen Sie explizit setzen. Wenn alle Zellen im Tableview die gleiche Höhe haben sollen, passen Sie diesen Wert im Größeninspektor an (siehe Abbildung 5.23).
Abbildung 5.23 Zellhöhe im Größeninspektor des Tableviews anpassen
Jede Zelle kann auch eine individuelle und sogar dynamische Höhe haben. Dazu müssen Sie allerdings die Höhe an zwei Stellen ändern. Zum einen müssen Sie die Höhe im Größeninspektor der Zelle setzen, damit Sie die Zelle in der gewünschten Höhe im Interface Builder bearbeiten können (siehe Abbildung 5.24).
Zum anderen müssen Sie dem Tableview über seine Delegate-Methode tableView:heightForRowAtIndexPath: die Höhe jeder Zelle mitteilen. Der Tableview braucht die Höhen, um die Gesamthöhe des Tabelleninhaltes zu berechnen.
Abbildung 5.24 Höhe der Zelle individuell anpassen
Die gesamte Implementierung der Methode tableView:cellForRowAtIndexPath: finden Sie in Listing 5.51. Dabei liefert die Methode entryAtIndexPath: das Modellobjekt des Tagebucheintrags zum Indexpfad.
- (UITableViewCell *)tableView:(UITableView *)inTableView
cellForRowAtIndexPath:(NSIndexPath *)inIndexPath {
DiaryEntryCell *theCell = (DiaryEntryCell *)[self.tableView
dequeueReusableCellWithIdentifier:@"dairyEntryCell"
forIndexPath:inIndexPath];
DiaryEntry *theEntry = [self entryAtIndexPath:inIndexPath];
theCell.imageControl.tag = inIndexPath.row;
[self applyDiaryEntry:theEntry toCell:theCell];
return theCell;
}
Listing 5.51 Erzeugung der Tabellenzellen im Fototagebuch
Das Control, auf das die Property imageControl der Zelle verweist, löst eine Aktion aus, die von dem enthaltenen Tagebucheintrag abhängt. Damit der Controller zu dieser Zeile den Eintrag wiederfinden kann, speichert er die Zeilennummer in der Property tag des Controls. Die Methode applyDiaryEntry:toCell: übergibt schließlich die Werte aus dem Tagebucheintrag an die entsprechenden Subviews der Zelle:
- (void)applyDiaryEntry:(DiaryEntry *)inEntry
toCell:(DiaryEntryCell *)inCell {
UIImage *theImage = [UIImage imageWithData:inEntry.icon];
[inCell setIcon:theImage];
[inCell setText:inEntry.text];
[inCell setDate:inEntry.creationTime];
}
Listing 5.52 Werte des Tagebucheintrags an die Zelle übergeben
Die Klasse für die Zelle besteht aus den drei Settern, die Listing 5.52 verwendet, dem Getter imageControl und drei Outlet-Propertys für das Bild sowie jeweils einem Label für das Datum und den Tagebuchtext.
Eigene Klassen für Tableview-Zellen
Mit eigenen Unterklassen von UITableViewCell können Sie sich den Zugriff auf selbstgestaltete Tableview-Zellen in der Regel stark vereinfachen. Sie weisen dem Zellprototyp die Klasse zu und verbinden die Outlets mit den gewünschten Elementen in dem Contentview der Zelle. Allerdings sollten Sie bei der Benennung der Outlet-Propertys darauf achten, dass Sie keine Namen verwenden, die schon die Klasse UITableViewCell benutzt. Beispielsweise kann eine Property textLabel in der Unterklasse zu unerwarteten Ergebnissen führen.
Sie können in der Unterklasse auch Action-Methoden implementieren und die Controls der Zelle damit verbinden. Allerdings sollte die Action-Methode in der Zelle die empfangenen Ereignisse nur weiterleiten und nicht verarbeiten, um das MVC-Muster nicht zu verletzen; gewöhnlich eignet sich Delegation hierfür ausgezeichnet.
Die Implementierung der Methoden der Klasse DiaryEntryCell sehen Sie in Listing 5.53.
- (UIControl *)imageControl {
return (UIControl *) self.iconView.superview;
}
- (void)setIcon:(UIImage *)inImage {
UIImageView *theView = self.iconView;
theView.image = inImage;
}
- (void)setText:(NSString *)inText {
self.entryTextLabel.text = inText;
}
- (void)setDate:(NSDate *)inDate {
NSDateFormatter *theFormatter =
[[NSDateFormatter alloc] init];
theFormatter.dateStyle = NSDateFormatterMediumStyle;
theFormatter.locale = [NSLocale currentLocale];
self.dateLabel.text = [theFormatter
stringFromDate:inDate];
}
Listing 5.53 Implementierung der Zellen für die Tagebucheinträge
5.5.4Zellprototypen per Programmcode bereitstellen

Sie können die Zellprototypen für einen Tableview auch über NIB-Dateien bereitstellen, wobei sich der Tableview trotzdem in einem Storyboard befinden kann. Für jeden Prototyp müssen Sie eine eigene XIB-Datei anlegen, die genau eine Tabellenzelle enthält. Diesen Prototyp können Sie dann über die Methode registerNib:forCellReuseIdentifier: zum Tableview hinzufügen, wobei der erste Parameter ein Objekt der Klasse UINib ist und der zweite die Kennung der Zelle. Das machen Sie am besten in der Methode viewDidLoad des Viewcontrollers, wie Listing 5.54 beispielhaft zeigt, das einen Zellprototyp aus der Datei Cell.nib lädt.
- (void)viewDidLoad {
[super viewDidLoad];
UINib *theNib = [UINib nibWithNibName:@"Cell" bundle:nil];
[self.tableView registerNib:theNib
forCellReuseIdentifier:@"cellIdentifier"];
}
Listing 5.54 Registrierung eines Zellprototyps über ein NIB
Der Tableview registriert dabei den Prototyp unter der Kennung, die Sie bei der Registrierung als zweiten Parameter angeben. Die Kennung für den Prototyp in Listing 5.54 lautet also cellIdentifier.
Zellprototypen in NIBs
Eigene NIB-Dateien für Zellprototypen eignen sich besonders gut, wenn Ihre App den gleichen Zelltyp in mehreren Tableviews verwenden muss. Anstatt in jedem Tableview gleichartige Prototypen anzulegen, legen Sie für jeden Prototyp jeweils eine XIB-Datei an, die Sie in die betroffenen Tableviews laden.
Außerdem können Sie Prototypen auch über Klassen definieren und über die Methode registerClass:forCellReuseIdentifier: beim Tableview registrieren. Die Klasse muss dabei eine Unterklasse von UITableViewCell sein und die Views im Contentview selbst anlegen. Der Tableview initialisiert neue Zellen aus der Klasse über die Methode initWithStyle:reuseIdentifier:; Sie können also diese Methode überschreiben, um die benötigten Views im Contentview anzulegen oder andere Initialisierungsschritte vorzunehmen.
Listing 5.55 legt einen Zellprototyp über diese Methode an.
- (void)viewDidLoad {
[super viewDidLoad];
[self.tableView registerClass:[TableCell class]
forCellReuseIdentifier:@"cellIdentifier"];
}
Listing 5.55 Prototypdefinition für Zellen über deren Klasse
5.5.5Der Target-Action-Mechanismus und Tabellenzellen

Wenn Sie den Zellprototyp im Tableview im Storyboard anlegen, können Sie auch Action-Verbindungen von den Controls des Prototyps zu den Objekten (z. B. Viewcontroller) in der Storyboard-Szene ziehen. Allerdings müssen Sie meistens in den Action-Methoden die Zelle oder deren Indexpfad ermitteln. Hierbei kann Ihnen die Methode indexPathForRowAtPoint: des Tableviews helfen, wenn Sie sich das UIEvent-Objekt an die Action-Methode mit übergeben lassen. Dazu berechnen Sie den Berührungspunkt relativ zum Tableview und übergeben diesen Wert an diese Methode, die Ihnen den Indexpfad der Zelle liefert, die sich an diesem Punkt befindet.
- (IBAction)action:(id)inSender forEvent:(UIEvent *)inEvent {
UITableView *theTableView = self.tableView;
UITouch *theTouch = inEvent.allTouches.anyObject;
CGPoint thePoint = [theTouch locationInView:theTableView];
NSIndexPath *thePath = [theTableView indexPathForRowAtPoint:thePoint];
...
}
Listing 5.56 Bestimmung des Indexpfads in einer Action-Methode
Wenn Sie das Bild in der Zelle antippen, öffnet der Controller einen Audioplayer-Controller, über den sich der Nutzer den Ton des Tagebucheintrags anhören kann. Dazu liegt das Bild in einem Control, dessen Touch-Up-Inside-Event die Action-Methode playSound: des Viewcontrollers aufruft.
Da die Controller von allen Zellen in der Tabelle die gleiche Action-Methode verwenden, muss diese herausbekommen, für welchen Tagebucheintrag Sie das Control gedrückt haben. Dazu bestimmen Sie den Indexpfad beispielsweise wie in Listing 5.56.
Falls Sie keine Abschnitte in Ihrer Tabelle haben, gibt es noch eine einfachere Möglichkeit: Verwenden Sie Tags für die Bestimmung der Zeilennummer, indem Sie dem Tag des Controls die Zeilennummer zuweisen. Den Tag-Wert müssen Sie in der Methode tableView:cellForRowAtIndexPath: wie in Listing 5.51 zuweisen.
- (IBAction)playSound:(id)inSender {
NSIndexPath *theIndexPath = [NSIndexPath indexPathForRow:
[inSender tag] inSection:0];
DiaryEntry *theItem = [self entryAtIndexPath:theIndexPath];
Medium *theMedium = [theItem mediumForType:kMediumTypeAudio];
if(theMedium != nil) {
AudioPlayerController *thePlayer = [self.storyboard
instantiateViewControllerWithIdentifier:
@"audioPlayer"];
thePlayer.audioMedium = theMedium;
[thePlayer presentFromViewController:self
animated:YES];
}
}
Listing 5.57 Aufruf des Audioplayers aus dem Tableview
5.5.6Zellen löschen
Tableviews verfügen bereits über eine eingebaute Möglichkeit, Tabellenzellen zu löschen. Wenn Sie mit dem Finger von rechts nach links über eine Tabellenzelle wischen, erscheint ein roter Button, über den Sie die Zelle entfernen können. Die Anzeige dieses Buttons durch Wischen schalten Sie im Tableview ein, indem Sie in seinem Delegate die Methode tableView:commitEditingStyle:forRowAtIndexPath: implementieren. Diese Methode verarbeitet das Drücken des Löschen-Buttons.
Wenn Sie den Löschen-Button in einer Zeile drücken, ruft der Tableview diese Methode auf. Sie müssen dann die entsprechenden Operationen zum Löschen der Zeile durchführen und die Anzeige in der Tabelle aktualisieren, was Sie im einfachsten Fall über einen Aufruf der Methode reloadData im Tableview erreichen. Durch diesen Aufruf baut der Tableview seine Zellen komplett neu auf.
Eine andere Möglichkeit besteht darin, dass der Tableview nur die Zeile entfernt, die Sie gelöscht haben. Dazu können Sie die Methode deleteRowsAtIndexPaths:withRowAnimation: verwenden. Der Photodiary-Controller löscht in der Delegate-Methode nur den Eintrag über Core Data. Die Aktualisierung des Tableviews erfolgt über Benachrichtigungen, was Abschnitt 5.7, »Inhalte teilen«, beschreibt.
- (void)tableView:(UITableView *)inTableView
commitEditingStyle:(UITableViewCellEditingStyle)inStyle
forRowAtIndexPath:(NSIndexPath *)inIndexPath {
if(inStyle == UITableViewCellEditingStyleDelete) {
DiaryEntry *theItem = [self entryAtIndexPath:
inIndexPath];
NSError *theError = nil;
[self.managedObjectContext deleteObject:theItem];
if(![self.managedObjectContext save:&theError]) {
NSLog("Unresolved error: %@", theError);
}
}
}
Listing 5.58 Löschen eines Tagebucheintrags
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.