Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Geleitwort
Vorwort
1 Hello iPhone
2 Grundlagen
3 Views und Viewcontroller
4 Alles unter Kontrolle
5 Daten, Tabellen und Controller
6 Models, Layer, Animationen
7 Programmieren, aber sicher
8 Datenserialisierung und Internetzugriff
9 Jahrmarkt der Nützlichkeiten
A Sicherer Entwicklungszyklus
Stichwort

Download:
- ZIP, ca. 49,9 MB
Buch bestellen
Ihre Meinung?

Spacer
Apps programmieren für iPhone und iPad von Klaus M. Rodewig, Clemens Wagner
Das umfassende Handbuch
Buch: Apps programmieren für iPhone und iPad

Apps programmieren für iPhone und iPad
Galileo Computing
1000 S., geb., mit DVD
39,90 Euro, ISBN 978-3-8362-1915-0
Pfeil 2 Grundlagen
Pfeil 2.1 Objektorientierte Programmierung
Pfeil 2.1.1 Objekte und Abstraktion
Pfeil 2.1.2 Vererbung
Pfeil 2.1.3 Überschreiben von Methoden und spätes Binden
Pfeil 2.1.4 Objektorientierung in Objective-C
Pfeil 2.1.5 Die Architektur von iOS-Programmen
Pfeil 2.2 Hefte raus, Klassenarbeit!
Pfeil 2.2.1 Controller und View in der Praxis
Pfeil 2.2.2 Modellbau
Pfeil 2.2.3 Initializer und Methoden
Pfeil 2.2.4 Vererbung
Pfeil 2.2.5 Kategorien
Pfeil 2.2.6 Protokolle
Pfeil 2.2.7 Vorwärtsdeklarationen
Pfeil 2.2.8 Kommunikation zwischen den Schichten
Pfeil 2.2.9 Delegation
Pfeil 2.3 Speicherverwaltung, Propertys und Key-Value-Coding
Pfeil 2.3.1 Der Stack
Pfeil 2.3.2 Der Heap
Pfeil 2.3.3 Referenzen zählen
Pfeil 2.3.4 Die Speicherverwaltungsregeln
Pfeil 2.3.5 Autorelease-Pools
Pfeil 2.3.6 Propertys und Accessoren
Pfeil 2.3.7 Key-Value-Coding
Pfeil 2.3.8 Abschließende Bemerkungen
Pfeil 2.4 Automatisches Referenzenzählen
Pfeil 2.4.1 Zurück in die Zukunft
Pfeil 2.4.2 Weakie und die starken Zeiger
Pfeil 2.4.3 Autorelease-Pools
Pfeil 2.4.4 Einzelgänger
Pfeil 2.4.5 Migration bestehender Projekte
Pfeil 2.5 Das Foundation-Framework
Pfeil 2.5.1 Mutables und Immutables
Pfeil 2.5.2 Elementare Klassen
Pfeil 2.5.3 Collections
Pfeil 2.6 Blöcke
Pfeil 2.6.1 Rückruffunktionen
Pfeil 2.7 Namenskonventionen
Pfeil 2.8 Zusammenfassung

Galileo Computing - Zum Seitenanfang

2.2 Hefte raus, Klassenarbeit!Zur nächsten Überschrift

Jetzt ist es an der Zeit, die Grundlagen aus dem vorangegangenen Abschnitt durch ein größeres Beispielprojekt zu illustrieren. Das hier vorgestellte Beispiel finden Sie auf der Buch-DVD sowie im Git-Repository zum Buch.

Versionsverwaltung

Sie werden eine Versionsverwaltung auch für kleine Projekte schätzen lernen, sobald Sie sich auch nur einmal im Dschungel Ihrer verschiedenen, über unzählige Ordner und Rechner verstreuten Versionen des Sourcecodes Ihres Projekts verirrt haben und aus dem wirren Wust keine übersetzungsfähige Version mehr erzeugen können. Für die Zusammenarbeit mit anderen Entwicklern ist eine Versionsverwaltung schlicht unverzichtbar.

Die Mutter aller Versionsverwaltungen ist CVS, das Concurrent Versions System, das lange Zeit der Standard gerade im Open-Source-Bereich war. Aufgrund zahlreicher konzeptioneller Eigenheiten ist CVS jedoch nicht mehr zeitgemäß und wurde weitgehend von den Systemen Subversion (SVN) und GIT abgelöst.

Ursprünglich hat der Linux-Erfinder Linus Torvalds GIT entwickelt, und es hat sich im Open-Source-Bereich gerade für große Projekte zum Standard ausgewachsen. Der Linux-Kernel, Android, Gnome, VLC und viele andere bekannte (und noch mehr unbekannte) Projekte verwenden GIT. Es hat sich bei der Verwaltung des Linux-Kernels bewährt und auf diese Weise seine Eignung auch für große Projekte unter Beweis gestellt.

Xcode unterstützt GIT erst ab Version 4, das es seitdem allerdings auch zur Standardversionsverwaltung erkoren hat.

Sie sollten möglichst jedes neue Xcode-Projekt als GIT-Repository einrichten, sofern Sie nicht ein anderes Repository verwenden wollen. Einen umfassenden Einblick in die Arbeit mit GIT bietet Kapitel 9, »Jahrmarkt der Nützlichkeiten«. Die Sourcen aller Beispiele im Buch finden Sie in dem öffentlich zugänglichen GIT-Repository Github unter der URL https://github.com/Cocoaneheads/iPhone.

Legen Sie für das folgende Beispiel ein neues Projekt in Xcode an. Öffnen Sie dazu den Projekt-Wizard, und erstellen Sie ein iOS-Projekt vom Typ Single View Application (siehe Abbildung 2.6).

Abbildung

Abbildung 2.6 Das Template für das neue Projekt

Geben Sie dem Projekt einen Namen, und achten Sie beim Anlegen des Projekts bitte darauf, dass die Option Automatic Reference Counting aktiviert ist, was der Voreinstellung entspricht. Das Feld Use Storyboards deaktivieren Sie für das Projekt, sodass die Einstellungen wie in Abbildung 2.7 aussehen.

Abbildung

Abbildung 2.7 Die Einstellungen beim Anlegen des Projekts


Galileo Computing - Zum Seitenanfang

2.2.1 Controller und View in der PraxisZur nächsten ÜberschriftZur vorigen Überschrift

Xcode legt automatisch einen Viewcontroller an, womit das erste MVC-Element schon im Projekt vorhanden ist – der Controller. Der Controller ist das zentrale Element der App; er stellt die Verbindung zwischen dem View und dem Modell her.

Nach Beenden des Projekt-Wizards öffnen Sie die Datei ViewController.xib, sodass die Hauptansicht von Xcode den View, den der Viewcontroller verwaltet, grafisch darstellt. Diesen Editor von Xcode bezeichnet man auch als Interface Builder. Da er noch leer ist, ziehen Sie aus der Objekt-Bibliothek rechts unten in der IDE einen Textview darauf (siehe Abbildung 2.8). Dabei darf er ruhig dessen komplette Fläche belegen. Sie erweitern dadurch die View-Schicht des Projekts, um darüber die Interaktion mit dem Benutzer zu ermöglichen.

Abbildung

Abbildung 2.8 Hinzufügen eines TextView zur GUI

Öffnen Sie anschließend die Dateien ViewController.xib und ViewController.h im Editor und Hilfseditor von Xcode, analog zum Beispiel in Kapitel 1 oder über die Tastenkombination alt + cmd + ¢. Danach ziehen Sie bei gedrückter ctrl-Taste eine Verbindung vom Textview in die Klassendeklaration in der Interface-Datei (siehe Abbildung 2.9).

Abbildung

Abbildung 2.9 So verbinden Sie den Textview mit dem Controller.

Geben Sie dem Outlet den Namen textView (siehe Abbildung 2.10), und weisen Sie Xcode durch eine Klick auf den Connect-Button an, die Verknüpfung und den entsprechenden Code zu erstellen.

Abbildung

Abbildung 2.10 Die Eigenschaften des neuen Outlets festlegen

Wie das Label bei der Applikation aus Kapitel 1 können Sie den Textview nun über das Outlet textView ansprechen, da Sie eine Verbindung vom Controller zum View erstellt haben. Dies machen Sie sich gleich zunutze, um Textmeldungen direkt auf der GUI der App auszugeben.

Wie Sie bereits wissen, verteilt sich der Quelltext einer Klasse in Objective-C auf zwei Dateien: auf die Headerdatei mit der Dateiendung .h und auf die Implementierungsdatei mit der Endung .m. Die Headerdatei enthält die Schnittstelle (das Interface) der Klasse nach außen, während die Implementierungsdatei die Umsetzung enthält, auf die andere Klassen keinen Zugriff haben.

Die Implementierungsdatei enthält den ausführbaren Code einer Klasse. Die Implementierung einer Klasse ist geheim. Sie geht nur den Programmierer etwas an. Damit andere Programmierer diese Klasse benutzen können, reicht es, die Headerdatei und die kompilierte Klasse weiterzugeben. Die Headerdatei deklariert, welche Methoden eine Klasse besitzt. Auf diese Weise ist es möglich, kompilierte Klassen zur freien Verwendung weiterzugeben, ohne den Code offenlegen zu müssen.

Attribute in der Klassendeklaration

Bei älteren Objective-C-Compilern musste die Klassendeklaration auch alle Attribute der Klasse enthalten. Da Attribute jedoch zu den Implementierungsdetails einer Klasse zählen, ist das eine Verletzung der Datenkapselung und somit des Geheimnisprinzips. Alle Compiler, die Apple mit Xcode 4 und neuer ausgeliefert hat, unterstützen Möglichkeiten, auf diese Attributdeklarationen zu verzichten.

Apple gibt Ihnen beispielsweise nicht den Quelltext seiner Frameworks. Was Sie auf der Festplatte Ihres Rechners finden, sind Headerdateien. Diese brauchen Sie für die Programmierung Ihrer eigenen Apps. Wie Apple die Klassen implementiert hat, bleibt Ihnen jedoch verborgen, denn die Implementierungsdateien liegen gut verwahrt auf den Servern in Cupertino. Das ist auch gut so, denn Sie sollten sich beim Programmieren nur auf die Teile des Codes verlassen, die Apple auch dokumentiert hat.

Kommen wir zurück zum Beispiel. Ändern Sie im Sinne einer besseren Übersichtlichkeit die Schriftgröße des eben zur GUI hinzugefügten Textviews, indem Sie den Textview anklicken und im Attributinspektor die Größe des Fonts auf 12.0 stellen (siehe Abbildung 2.11).

Der Viewcontroller, der im Code durch die Headerdatei ViewController.h und die Implementierungsdatei ViewController.m repräsentiert wird, besitzt verschiedene Methoden, die im Zusammenhang mit seinem Lebenszyklus stehen. Bevor Sie eine dieser Methoden benutzen, implementieren Sie zunächst eine eigene, neue Methode, die dazu dient, Textausgaben in den Textview zu schreiben. Öffnen Sie dazu die Datei ViewController.m; und fügen Sie vor dem Schlüsselwort @end die folgende Methode ein:

-(void)writeLog:(NSString *)inLogString{
NSLog(@"[+] %@.%@", self, Zeilenumbruch
NSStringFromSelector(_cmd));
NSDateFormatter *theFormatter = Zeilenumbruch
[[NSDateFormatter alloc] init];

[theFormatter setDateFormat:@"HH:mm:ss.SSS"];
[self.textView setText:[NSString Zeilenumbruch
stringWithFormat:@"%@\n%@ [+] %@", Zeilenumbruch
[self.textView text],
[theFormatter stringFromDate:[NSDate date]], Zeilenumbruch
inLogString]];
}

Listing 2.7 Ausgabe einer Meldung in den Textview

Abbildung

Abbildung 2.11 Ändern der Schriftgröße des Textviews

Die Methode dient dazu, Meldungen mit einem Zeitstempel in den Textview zu schreiben. Anschließend erweitern Sie die Methode viewDidLoad um zwei Anweisungen, sodass die Methode wie folgt aussieht:

- (void)viewDidLoad {
[super viewDidLoad];
[self.textView setText:@];
[self writeLog:@"viewDidLoad"];
}

Listing 2.8 Ausgabe der ersten Meldung

Wenn Sie die App anschließend mit cmd + R oder über den Run-Button links oben in der Xcode-Werkzeugleiste übersetzen und starten, sollten Sie im Simulator die Ausgabe aus Abbildung 2.12 sehen.

Abbildung

Abbildung 2.12 Log-Ausgabe im UITextView

Was haben Sie jetzt eigentlich programmiert? Gehen wir den neuen Code einfach zeilenweise durch:

-(void)writeLog:(NSString *)logString {

Hiermit haben Sie eine Methode mit dem Namen writeLog: angelegt, die als Rückgabetyp void hat, also nichts zurückgibt. Die Methode akzeptiert einen Parameter vom Typ NSString, also eine Zeichenkette, mit dem Namen inLogString.

NSLog(@"[+] %@.%@", self, NSStringFromSelector(_cmd));

Der NSLog-Befehl in der zweiten Zeile dient zur Ausgabe von Text in die Systemkonsole. Der erste Parameter für NSLog entspricht den von C bekannten Formatstrings, also einer Zeichenkette mit Platzhaltern, gefolgt von den Daten, die an die Stelle der Platzhalter kommen sollen. Wie Sie ja bereits wissen, macht der Klammeraffe vor dem ersten Anführungszeichen aus der Zeichenkette ein NSString-Objekt.

Der Platzhalter %@ steht für ein Objekt und ist somit eine Erweiterung der Format-Platzhalter von C, wo es ja keine Objekte gibt. Die Daten für die beiden %@-Platzhalter liefern die Ausdrücke self und NSStringFromSelector(_cmd). Die Variable self steht Ihnen in jeder Methode zur Verfügung und verweist auf das aktuelle Objekt. Das gilt übrigens auch für Klassenmethoden, wo self auf das entsprechende Klassenobjekt verweist. Die Variable _cmd steht Ihnen ebenfalls in jeder Methode zur Verfügung und enthält jeweils deren Selektor. Diese Funktion NSStringFromSelector wandelt nun einen Selektor in eine Zeichenkette um. Der Funktionsaufruf liefert also immer den Namen der aktuellen Methode; in diesem Fall ist das »writeLog:«.

Super Nachricht, ey!

Listing 2.8 verwendet neben self noch die Variable super, die ebenfalls das aktuelle Objekt bezeichnet. Im Gegensatz zu self ignoriert super jedoch alle Methoden der aktuellen Klassenimplementierung und verwendet nur die Methoden der Superklassen; in diesem Fall ist das nur die Klasse UIViewController. Das ist an dieser Stelle auch notwendig, da eine Endlosrekursion entstehen würde, wenn self der Empfänger der Nachricht viewDidLoad wäre. Die Methode würde sich dadurch immer wieder selbst aufrufen.

Sie können diese Ausgabe sehen, wenn Sie (wie in Abbildung 2.13 gezeigt) die Konsolenansicht in Xcode öffnen und anschließend die App erneut ausführen. Sie erhalten in der Konsolenansicht folgende Ausgabe:

2012-10-07 12:51:04.383 Kapitel2[8056:c07] [+] <ViewController: 0x715f240>.writeLog:

Dabei hat die NSLog die Variable self in die Zeichenkette <ViewController 0x715f240> umgewandelt, die ihren Klassennamen und die Speicherstelle des Objekts im Hauptspeicher angibt.

Abbildung

Abbildung 2.13 Öffnen der Konsolenansicht

Diese Art des Loggings ist beim Verfolgen des Programmablaufs extrem hilfreich, insbesondere dann, wenn Sie Fehler suchen. Die Konsole bezeichnet man nicht umsonst auch als »Debugger für Arme«.

Schauen wir uns die nächste Anweisung der Methode writeLog: an:

NSDateFormatter *theFormatter = Zeilenumbruch
[[NSDateFormatter alloc] init];

Hier erzeugen Sie ein neues Objekt der Klasse NSDateFormatter und weisen es der Variablen theFormatter zu. Dazu rufen Sie die Klassenmethode alloc dieser Klasse auf und senden der dadurch neu erzeugten Instanz anschließend die Nachricht init. Über diese zwei Aufrufe erzeugen Sie ja in vielen Fällen neue Objekte.

NSDateFormatter ist eine Klasse, mit der Sie Zeitstempel formatieren können; Sie wandeln damit also einen Zeitstempel in eine Zeichenkette mit einem vorgegebenen Aufbau um. Jede Log-Ausgabe auf dem Textview soll eine Zeitangabe enthalten, die die Stunde, Minute, Sekunde und Millisekunde umfasst. Dazu legen Sie in der nächsten Zeile über den Methodenaufruf setDateFormat: das gewünschte Format fest. Den Aufbau dieser Zeichenkette beschreiben Sie ebenfalls über eine Zeichenkette:

[theFormatter setDateFormat:@"HH:mm:ss.SSS"];

Dabei stehen »HH«, »mm« und »ss« jeweils für die zweistellige Ausgabe von Stunden, Minuten und Sekunden, und die Angabe »SSS« wandelt die Millisekunden in eine dreistellige Zeichenkette um.

Danach schreibt die Methode die Nachricht in den Textview. Dabei soll sie jedoch nicht den bestehenden Inhalt löschen, sondern nur den neuen anhängen. Das erreicht sie, indem sie den alten Inhalt ausliest, ihn über die Methode stringWithFormat: mit dem neuen Wert verknüpft und ihn über die Methode setText: wieder an den Textview übergibt. Dabei funktioniert die Klassenmethode stringWithFormat: ähnlich wie die Funktion NSLog: Sie übergeben im ersten Parameter ein Format mit Platzhaltern, deren Werte Sie in den folgenden Parametern festlegen. Die Escape-Sequenz \n sorgt übrigens für einen Zeilenvorschub zwischen dem alten Inhalt des Textviews und der neuen Meldung.

[self.textView setText:[NSString stringWithFormat: Zeilenumbruch
@"%@\n%@ [+] %@", [self.textView text],
[theFormatter stringFromDate:[NSDate date]], logString]];

Für den ersten Wert verwenden Sie den oben erzeugten Formatter mit der Methode stringFromDate:, die einen Zeitstempel in eine Zeichenkette mit dem vorgegebenen Aufbau umwandelt. Den dafür notwendigen Zeitstempel, der die aktuelle Zeit enthält, erzeugen Sie über die Klassenmethode date der Klasse NSDate. Da diese Methode ein neues, bereits initialisiertes Objekt erzeugt, zählt man sie zu den Convenience-Konstruktoren. Das sind Klassenmethoden, die die Objekterzeugung gegenüber alloc und init etwas vereinfachen.

Damit ist die Methode zum Schreiben von Log-Ausgaben in den Textview komplett. Jetzt betrachten Sie noch die zweite Änderung im Viewcontroller, die Sie vorgenommen haben – die Methode viewDidLoad:.

- (void)viewDidLoad {

Die erste Zeile eröffnet die Methodendefinition von viewDidLoad:. Die nächste Zeile der Methode ruft die gleichnamige Methode in der Superklasse auf. Damit führen Sie die Anweisungen in der Methode viewDidLoad: der Superklasse aus.

    [super viewDidLoad];

Damit der User diesen Beispieltext (oder einen anderen Text, der zufällig im Textview steht) beim Laden der App nicht angezeigt bekommt, löschen Sie mit der nächsten Zeile den Inhalt des Textviews, indem Sie dem Textview die Nachricht setText: mit einer leeren Zeichenkette senden:

    [self.textView setText:@""];

In der nächsten Zeile ruft die Methode writeLog: auf und übergibt ihr als Parameter den Namen der aktuellen Methode als Zeichenkette, also viewDidLoad. Der Adressat self in diesem Aufruf bezeichnet das aufrufende Objekt selber; es schickt also eine Nachricht zum Aufruf von writeLog: an sich. Selbstgespräche von Objekten sind in objektorientierten Programmiersprachen nicht unüblich.

    [self writeLog:[NSString stringWithFormat:@"%@", Zeilenumbruch
NSStringFromSelector(_cmd)]];

Galileo Computing - Zum Seitenanfang

2.2.2 ModellbauZur nächsten ÜberschriftZur vorigen Überschrift

Controller und View sind vorhanden, nun fehlt noch die Modell-Schicht, um MVC komplett zu machen. Viele Apps kommen auch ohne Modell aus, da sie beispielsweise nur extrem geringe Datenmengen verwalten. Bei solchen Anwendungen übernimmt dann häufig der Controller auch noch die Aufgaben des Modells. Ein Beispiel dafür ist die Wecker-App aus dem dritten Kapitel. Sie besitzt kein eigenständiges Modell, weil ihre Daten nur aus der aktuellen Zeit und der Weckzeit bestehen. Eigene Modellklassen dafür würden diese App unnötig verkomplizieren.

Das Beispielprojekt in diesem Kapitel soll jedoch ein Modell mit einer eigenen Klasse erhalten, die Sie nun zum Projekt hinzufügen. Wie bereits erwähnt wurde, ist in Objective-C eine Klasse die Konstruktionszeichnung für ein Objekt. Klassen können keine Daten speichern oder Berechnungen anstellen, Sie müssen dafür erst Objekte erzeugen. Dies erfolgt, wie Sie bereits gesehen haben, über einen Methodenaufruf an die Klasse des Objekts (also eine Klassenmethode), mit der das Klassenobjekt angewiesen wird, ein Objekt zu erzeugen. Die dazu aufgerufene Methode trägt den Namen alloc. Alle Klassen, die von NSObject abstammen, kennen diese Methode, da die Klasse NSObject sie als Klassenmethode implementiert.

Die Klassenmethode alloc

In der Datei NSObject.h können Sie sehen, dass alloc eine Klassenmethode ist:

+ (id)alloc;

Das vorangestellte Plus-Zeichen weist alloc als Klassenmethode aus. Instanzmethoden, also Methoden von Objekten, beginnen mit einem Minus-Zeichen, z. B.:

- (id)init;

alloc erzeugt ein Objekt der jeweiligen Klasse. Das bedeutet, dass alloc den Speicher für das Objekt reserviert, alle Attribute auf 0 beziehungsweise nil setzt und einen Zeiger auf das Objekt zurückliefert.

Ein mit alloc erzeugtes Objekt ist also sozusagen leer. Um das Objekt direkt nach der Erzeugung zur Verwendung vorzubereiten (es also direkt mit sinnvollen Daten zu füllen), sollte unmittelbar nach dem Aufruf der Klassenmethode alloc der Aufruf der Methode init oder einer anderen Initialisierungsmethode erfolgen. Eine Initialisierungsmethode ähnelt den aus anderen Programmiersprachen bekannten Konstruktoren. Alle Subklassen von NSObject erben oder überschreiben die Methode init.

Neben init sind auch andere Initialisierungsmethoden mit Parameterübergabe möglich. Dabei ist es eine Konvention, die Sie auch unbedingt einhalten sollten, dass Initialisierungsmethoden mit dem Präfix init beginnen; typische Namen sind beispielsweise initWithString:, initWithFrame: oder initWithNibNamed:bundle:. Im Gegensatz zu anderen Methoden sollten und dürfen Sie die Initialisierungsmethoden nur genau einmal nach der Erzeugung des Objekts aufrufen, und da hilft Ihnen diese Namenskonvention, diese Methoden besser zu erkennen.

Kehren Sie dazu zu dem oben angelegten Projekt zurück, und fügen Sie eine neue Klasse hinzu. Dies können Sie entweder über den Menüpunkt FileNewFile ... tun oder mit einem Mausklick über den Utilities-Bereich von Xcode. Der Utilities-Bereich, den Sie beim Einrichten des Projekts bereits verwendet haben, verfügt im rechten unteren Bereich des Xcode-Fensters über eine File Template Library, eine Sammlung von Schablonen für neue Dateien. Suchen Sie in dieser Library nach der Vorlage für eine Objective-C class für Cocoa Touch, und ziehen Sie diese links in die Dateiübersicht von Xcode.

Abbildung

Abbildung 2.14 Eine Schablone für eine neue Klasse auswählen...

Am besten fügen Sie die neuen Dateien in die gleiche Gruppe (das sind die gelben Ordner in der Dateiübersicht) wie die anderen Klassen ein (siehe Abbildung 2.15).

Abbildung

Abbildung 2.15 ... und per Drag & Drop zum Projekt hinzufügen

Nach dem Ziehen in die Dateiübersicht öffnet sich ein Dialog, in dem Sie den Namen der neu hinzugefügten Klasse vergeben können. Auch wenn als Datei-Endung in diesem Dialog nur .m zu sehen ist, erzeugt Xcode automatisch eine Header- und eine Implementierungsdatei – Sie müssen sich nicht darum kümmern. Geben Sie der Klasse den Namen »Model«, und schließen Sie den Dialog (siehe Abbildung 2.16). Das Hinzufügen der Klasse zum Xcode-Projekt ist damit abgeschlossen.

Abbildung

Abbildung 2.16 Ein Name für die neue Klasse

Die Klasse soll nicht nur durch ihre Anwesenheit glänzen, sondern Eigenschaften und Methoden besitzen. Traditionell werden Attribute in Objective-C im Interface deklariert:

@interface Model : NSObject {
@private
NSString *;
NSDate *creation;
NSString *name;
NSMutableArray *objects;
}
@end

Listing 2.9 Klassendeklaration mit Attributen

Das Schlüsselwort @private vor diesem Attribut kennzeichnet, dass nur die eigene Klasse die Attribute lesen darf.

Halt, noch nicht abschreiben!

Wie bereits erwähnt wurde, sind Attributdeklarationen in der Schnittstelle eine Verletzung des Geheimnisprinzips, da sie Implementierungsdetails ausplaudern. Da es jedoch früher nicht möglich war, ohne dieses Konstrukt auszukommen, finden Sie auch noch heute sehr viele Klassendeklarationen mit Attributen. Beispielsweise gehören auch noch sehr viele Klassen aus Cocoa Touch dazu. Sie erfahren gleich, wie Sie auf diese Attributdeklarationen verzichten können, und Listing 2.9 soll lediglich veranschaulichen, wie solche Deklarationen aussehen.

Da der Zugriff auf diese Attribute im Sinne der Datenkapselung nicht direkt, sondern über entsprechende Methoden – die Accessoren – erfolgt, müssen Sie die entsprechenden Methoden schreiben. Die Methode zum Lesen eines Attributs bezeichnet man dabei als Getter, die Methode zum Setzen als Setter. Das Programmieren von Gettern und Settern ist reine Fleißarbeit. Für das Lesen des Attributs name ist beispielsweise die folgende Methode notwendig:

-(NSString *)name {
return name;
}

Listing 2.10 Ein Getter in Objective-C

Der Setter ist komplizierter, was an der Speicherverwaltung der Objective-C-Runtime liegt. Darauf gehen wir später noch genauer ein; hier können wir darauf verzichten, da es einen einfacheren Weg gibt, Accessoren zu implementieren.

Seit Objective-C 2.0 gibt es mit den Propertys eine bessere Alternative zu Attributen und selbst geschriebenen Accessoren. Bei der Verwendung von Propertys kann der Compiler selbstständig Accessoren anlegen, wodurch für Sie lästige Tipp-Arbeit entfällt. Das Interface das Klasse Model ändert sich durch die Verwendung von Propertys wie folgt:

@interface Model : NSObject
@property(copy) NSString *status;
@property(strong) NSDate *creation;
@property(copy) NSString *name;
@end

Listing 2.11 Klassendeklaration mit Propertys

Das Attribut objects haben wir ersatzlos aus der Klassendeklaration gestrichen, weil es keine Property abbilden soll und wir seine Deklaration deshalb in die Implementierungsdatei verschoben haben, wie Sie Listing 2.12 entnehmen können.

@implementation Model {
@private
NSMutableArray *objects;
}

Listing 2.12 Attributdeklaration am Implementierungsblock

Die vier restlichen Attribute lassen sich problemlos als Propertys deklarieren. Dies macht das Schlüsselwort @property deutlich, auf das die in Klammern gesetzten Optionen, der Typ und der Name der Property folgen. Mehr als das müssen Sie nicht tun, um Propertys zu einer Klasse hinzuzufügen.

@synthesize

Wenn Sie Xcode in einer Version bis 4.4 verwenden, müssen Sie den Compiler – neben der Deklaration über @property im Interface – auch noch über die Direktive @synthesize in der Implementierungsdatei anweisen, die Property zu implementieren. Zu der Deklaration

@property (copy) NSString *name;

in der Headerdatei mussten Sie also in der Implementierungsdatei ein entsprechendes

@synthesize name;

einfügen. Wenn Sie den Compiler LLVM 4.1 nutzen, den Apple mit Xcode 4.5 ausliefert, ist das glücklicherweise nicht mehr notwendig. Jetzt reicht die Property-Deklaration vollkommen aus. Andererseits können Sie auch die @synthesize-Anweisung noch getrost in den Implementierungsblock schreiben, damit Ihr Code auch zu älteren Compilerversionen kompatibel bleibt. Für die Klasse Model sehen diese Anweisungen so aus:

@synthesize ;
@synthesize name;
@synthesize creation;

Der Compiler erzeugt jetzt für jedes Attribut eine Getter-Methode mit dem Namen des Attributes und eine Setter-Methode, die den Namen mit vorangestelltem Präfix set besitzt, also für die Property status die Methoden status und setStatus. Außerdem erzeugt er noch ein Attribut mit dem Namen der Property, das Sie in Ihren Methoden verwenden können.

Die synthetischen Getter und Setter legt der Compiler jedoch nur an, wenn Sie diese nicht selber anlegen. Sie können also trotz @synthesize auch eigene Getter und Setter implementieren. Wenn Sie beide Zugriffsmethoden selbst implementieren, müssen Sie @synthesize verwenden, wenn Ihnen der Compiler das Attribut anlegen soll.

Die Punktnotation

Objective-C erlaubt für den Aufruf von Zugriffsmethoden eine alternative Syntax. Anstelle der eckigen Klammern verwenden Sie hierfür einen Punkt, und bei den Settern lassen Sie das Präfix set weg. Die Anweisung

[[self user] setName:[textField text];

können Sie mit der Punktnotation auch kurz als

self.user.name = textField.text;

schreiben. Es ist allerdings auch hier ein Unterschied, ob Sie self.user oder einfach nur user schreiben. Während [self user] und self.user Methodenaufrufe sind, ist user nur ein direkter Attributzugriff ohne Methodenaufruf. Zwischen der Notation mit eckigen Klammern und der Punktnotation besteht also nur ein syntaktischer, jedoch kein semantischer Unterschied, wie er zwischen self.user und user besteht.


Galileo Computing - Zum Seitenanfang

2.2.3 Initializer und MethodenZur nächsten ÜberschriftZur vorigen Überschrift

Die Klasse Model verfügt nun über Eigenschaften mit den entsprechenden Zugriffsmethoden. Es wäre jetzt schön, wenn ein Objekt direkt nach seiner Erzeugung, also in der Initialisierung, sinnvolle Werte für seine Propertys erhielte. Das Objekt erst zu initialisieren und danach von außen die Property-Werte zu setzen, ist häufig umständlich oder auch nicht sinnvoll. Sie können allerdings die Methode init der Klasse NSObject überschreiben, um ein Objekt mit den gewünschten Werten vorzubelegen.

Xcode bringt praktischerweise nicht nur Datei-Templates mit, sondern auch Code-Schnipsel (auch Snippets genannt), die Sie direkt in Ihren Code ziehen können. Unter diesen Snippets befindet sich auch die Vorlage für eine init-Methode. Wählen Sie in der Utilities-Spalte rechts in Xcode-Fenster unten die Code Snippet Library aus (siehe Abbildung 2.17), und geben Sie in das Suchfeld ganz unten im Fenster »init« ein. Xcode zeigt Ihnen alle verfügbaren Snippets an, die auf diesen Suchbegriff passen.

Abbildung

Abbildung 2.17 Auswahl des Snippets für eine »init«-Methode

Wählen Sie das Snippet Objective-C init Method aus, und ziehen Sie es in die Im-plementierungsdatei der Klasse Model in der Datei Model.m zwischen die @implementation- und die @end-Anweisung. Alternativ können Sie an dieser Stelle auch einfach ein »i« tippen, das Xcode dann in grauer Schrift zum Wort »init« ergänzt. Die IDE schlägt Ihnen dadurch vor, Ihre Eingabe durch den Schnipsel für init zu ersetzen, was Sie durch Drücken der Eingabetaste bestätigen können.

Abbildung

Abbildung 2.18 Ziehen Sie die Methode direkt in die Implementierung.

Die Implementierung sollte nun so wie in Listing 2.13 aussehen. Durch Drücken der ÿ_-Taste bewegen Sie den Cursor an die Stelle <#initializations#>, die der Editor auch hervorhebt.

#import
"Model.h"

@implementation Model {
@private
NSMutableArray *objects;
}

@synthesize ;
@synthesize name;
@synthesize creation;

- (id)init
{
self = [super init];
if (self != nil) {
<#initializations#>
}
return self;
}
@end

Listing 2.13 Implementierung der Klasse »Model«

Die Importanweisung

Die Präprozessordirektive #import ist eine Objective-C-Erweiterung, die wie das aus C bekannte #include eine Headerdatei einbindet. Im Gegensatz zu #include vermeidet #import jedoch das versehentliche mehrfache Einbinden von Headerdateien durch verschachtelte #import-Anweisungen. Sie brauchen also nicht zu überprüfen, ob Sie eine Datei bereits importiert haben, wie das bei #include notwendig ist. Diese Eigenschaft ist sehr nützlich, und so unterstützen auch alle mit Xcode ausgelieferten C-Compiler (gcc, clang und llvm-gcc) diese Direktive.

Bei <#initializations#> erweitern Sie die neue Methode um zwei Anweisungen. Die erste weist dem Attribut creation das aktuelle Datum zu, und die andere initialisiert das Attribut objects. Nach dieser Änderung sollte die Methode also wie folgt aussehen:

- (id)init
{
self = [super init];
if (self != nil) {
self.creation = [NSDate date];
objects = [[NSMutableArray alloc] init];
}
return self;
}

Listing 2.14 Initialisierung von Property- und Attributwerten

Die Property creation enthält nun also automatisch den Zeitpunkt, an dem das Programm das Objekt erzeugt hat, und durch die Zuweisung eines Objekts der Klasse NSMutableArray können Sie über dieses Attribut weitere Objekte speichern.

Das Objekt Model besitzt überdies noch die Property name, die einen eindeutigen Bezeichner aufnehmen soll, falls die App verschiedene Modellobjekte erzeugt. Das Zuweisen des Namens kann natürlich auch von außen über den Aufruf des entsprechenden Setters erfolgen. Allerdings müssen Sie dazu nach dem Erzeugen und Initialisieren des Objekts eine weitere Nachricht zum Einrichten des Objekts senden. Da sich dieser Schritt jedoch auch zur Objektinitialisierung zählen lässt, können Sie einen weiteren Initializer mit einem Parameter implementieren. Diese neue Methode deklarieren Sie zunächst in der Headerdatei:

-(id)initWithName:(NSString*)inName;

Listing 2.15 Deklaration eines Initializers mit Parametern

Sie besitzt einen Parameter für den Namen, und ihre Implementierung sieht wie folgt aus:

-(id)initWithName:(NSString*)inName {
self = [self init];
if(self != nil) {
self.name = inName;
}
return self;
}

Listing 2.16 Initializer mit Parameterübergabe

Sie ruft zuerst die init-Methode der Klasse Model auf, um auch die Property creation und das Attribut objects zu initialisieren. Anstatt die Variable self über den Aufruf der init-Methode der Superklasse zu initialisieren, dürfen Sie also auch einen anderen Initializer aus der Klasse Model verwenden, wenn dabei kein Zyklus entsteht. Das würde beispielsweise passieren, wenn init die Variable self über self = [self initWithName:@"Modell"]; initialisieren würde. Dann würden sich die Methoden gegenseitig aufrufen, und es entstünde eine Endlosschleife, die das Betriebssystem erst abbricht, wenn der Speicher voll ist.

Designierter Initializer

Die Klasse Model verfügt jetzt über mehrere Initializer, von denen nur einer die eigentliche Initialisierungsaufgabe übernimmt. Diesen Initializer nennt man den designierten Initializer. Der designierte Initializer muss auch immer die Initialisierung der Superklasse ausführen, da es andernfalls eine Initialisierungsmöglichkeit gäbe, die das nicht macht. Das würde zu unvollständig initialisierten Objekten führen. Jeder Initializer muss also – direkt oder indirekt – einen Initializer der Superklasse aufrufen.

Mit den Methoden init beziehungsweise initWithName können Sie nun neue Objekte der Klasse im Programm erzeugen. Der Viewcontroller ist die koordinierende Instanz der App, daher ist er auch eine prädestinierte Stelle für die Erzeugung und Verwaltung eines Modells. Fügen Sie in der Headerdatei ViewController.h eine Importanweisung für die Klasse Model hinzu: #import "Model.h". Dadurch machen Sie das Modell dem Controller bekannt, und Sie können die Klasse Model und deren Methoden in der Header- und Implementierungsdatei des Controllers verwenden, ohne Compilerwarnungen und Fehler zu provozieren.

Anschließend fügen Sie eine neue Property mit dem Namen model zu der Klasse ViewController hinzu. Diese Property soll auf das zu erzeugende Objekt der Klasse Model verweisen:

@property (strong) Model *model;

In der Implementierungsdatei ViewController.m fügen Sie in der Zeile mit der von Xcode bereits erzeugten @synthesize-Anweisung noch die neue Property hinzu:

@synthesize model;

Damit ist die Deklaration der Property model abgeschlossen. Sie können sie nun verwenden, müssen sie allerdings vor der erstmaligen Verwendung noch initialisieren. Damit das Objekt vor der ersten Anzeige des Viewcontrollers zur Verfügung steht, fügen Sie in der Datei ViewController.m in der Methode viewDidLoad den folgenden Aufruf hinzu:

self.model = [[Model alloc] initWithName:@"LoremIpsum"];

Listing 2.17 Initialisierung der Property für das Modell

Listing 2.17 verwendet also Ihren soeben erstellten Initializer initWithName:, an den Sie die Zeichenkette @"LoremIpsum" als Namen des Modells übergeben. Ansonsten erfolgt die Erzeugung des Objekts wie bisher: Zuerst erfolgt der Aufruf von alloc und danach der Aufruf der Initialisierungsmethode.

Abbildung 2.19 zeigt ein Sequenzdiagramm des Kommunikationsablaufs. Im ersten Schritt sendet der Viewcontroller die Nachricht alloc an die Klasse Model. Sie erzeugt ein neues, unitialisiertes Objekt dieser Klasse und liefert es zurück 2. In Schritt 3 sendet der Viewcontroller die Nachricht initWithName: mit dem Parameter @"LoremIpsum" an dieses frisch erzeugte Objekt. Diese Methode liefert den Empfänger der initWithName:-Nachricht, also das neu erzeugte Modellobjekt, zurück 4.

Abbildung

Abbildung 2.19 Ablauf der Objekterzeugung

Um zu überprüfen, ob dieses Konstrukt den Praxistest besteht, fragen Sie nach der Erzeugung des Modells dessen Property name ab und übergeben das Ergebnis an die Methode writeLog:, um es im View der App anzuzeigen:

[self writeLog:[NSString stringWithFormat:@"Model.name: %@", Zeilenumbruch
[self.model name]]];

Lassen Sie das Programm übersetzen und ausführen. Dann sollte die Anzeige aus Abbildung 2.20 erscheinen.

Abbildung

Abbildung 2.20 Die Ausgabe nach der Initialisierung

Voilà, die Initialisierung über den erweiterten Initializer mit Parameter funktioniert. Das Modell soll – als Stellvertreter einer wie auch immer gearteten Programmlogik in »echten« Apps – im weiteren Verlauf des Kapitels Objekte der Klasse Droid verwalten. Sie fügen zunächst also eine weitere Klasse zum Projekt hinzu. Dies erfolgt wie bereits bei der Klasse Model beschrieben, indem Sie die entsprechende Vorlage aus der Bibliothek in die Dateiansicht Ihres Projekts ziehen. Geben Sie der neuen Klasse den Namen Droid.

Die Klasse erhält eine Property und eine Methode initWithID:

@property(copy) NSString *droidID;

-(id)initWithID:(NSInteger)inID;

Fügen Sie diese Deklarationen in die Headerdatei Droid.h der Klasse ein, und öffnen Sie anschließend die Implementierungsdatei Droid.m.

Zahltypen in Objective-C

In Objective-C können Sie Zahlen über elementare Datentypen, wie int, float, NSInteger, NSUInteger u. a., oder über die Klasse NSNumber darstellen. Ein elementarer Datentyp hat keine Methoden, und Sie weisen seinen Variablen einfach einen Wert zu, beispielsweise:

int theRadius = 3;
NSInteger theDiameter = 2 * theRadius;
float theCircumference = theDiameter * 3.14159;

Im Gegensatz dazu erzeugen Sie Objekte von NSNumber wie andere Objekte auch. Da Sie ein NSNumber-Objekt jedoch nach seiner Erzeugung nicht mehr ändern können, müssen Sie dessen Wert schon bei der Initialisierung festlegen:

NSNumber *theRadius = [[NSNumber alloc] initWithInt:3];
NSNumber *theDiameter = [[NSNumber alloc] initWithInteger: Zeilenumbruch
2 * [theRadius intValue]];
NSNumber *theCircumference = [[NSNumber alloc] Zeilenumbruch
initWithInteger:[theDiameter integerValue] * 3.14159];

Wie Sie an dem Beispiel sehen können, kann ein NSNumber-Objekt sowohl ganze Zahlen als auch Fließkommawerte aufnehmen.

Dort sorgen Sie durch die @synthesize-Anweisung für die automatische Erzeugung der Property und implementieren die Methode initWithID: wie folgt:

@implementation Droid

@synthesize droidID;

- (id)initWithID:(NSInteger)inID {
self = [super init];
if(self != nil){
self.droidID = [NSString stringWithFormat: Zeilenumbruch
@"0xDEADBEEF%i", inID];
}
return self;
}

Die Methode initWithID: ruft die Initialisierung der Superklasse auf und weist der Property droidID anschließend die Zeichenkette »0xDEADBEEF« zu, auf die eine Zahl folgt, die der Methode übergeben wurde.

Das Array objects des Modells soll Objekte der Klasse Droid aufnehmen. Dazu importieren Sie zunächst die Headerdatei Droid.h in der Implementierungsdatei Model.m, indem Sie am Anfang hinter der Anweisung #import "Model.h" die Anweisung #import "Droid.h" einfügen. Die Methode updateDroids: soll einen neuen Droiden des Modells anlegen oder den letzten zerstören.

- (void)updateDroids:(NSInteger)inValue {
if(inValue > [objects count]){
Droid *theDroid;

theDroid = [[Droid alloc] initWithID:inValue];
[objects addObject:theDroid];
}
else if(inValue < [objects count]) {
[objects removeLastObject];
}
}

Listing 2.18 Verwaltung der Droiden

Die Methode vergleicht die übergebene Zahl mit der Anzahl der Objekte im Array objects, die Sie über die Methode count ermitteln können. Dabei dient das Array dazu, mehrere Objekte zu speichern. Die Bedingung inValue > [objects count] ist also wahr, wenn der Wert in inValue größer ist als die Anzahl der Objekte in objects. In diesem Fall legt die Methode einen neuen Droiden an und fügt ihn über die Methode addObject: der Klasse NSMutableArray in das Array objects ein; das ist die letzte Zeile des ersten if-Blocks. Der zweite if-Block löscht das letzte Objekt in dem Array über die Methode removeLastObject, wenn der Parameterwert kleiner als die Anzahl der Objekte im Array ist.

Die neue Methode soll später auch der Viewcontroller – also eine andere Klasse – aufrufen können. Dazu müssen Sie sie in der Headerdatei deklarieren, da sie zurzeit nur in der Implementierungsdatei bekannt ist. Fügen Sie die Methodendeklaration in die der Klasse ein, wie es Listing 2.19 hervorgehoben darstellt:

@interface Model : NSObject

@property(copy) NSString *;
@property(strong) NSDate *creation;
@property(copy) NSString *name;

- (void)updateDroids:(NSInteger)inValue;

@end

Listing 2.19 Methodendeklaration in der Klasse »Model«

Um die neue Methode auszuprobieren, erweitern Sie den View in der Datei ViewController.xib Ihres Projekts so wie in Abbildung 2.21 gezeigt. Dazu selektieren Sie zunächst den Textview, sodass Sie die Markierungen für die Größenänderung sehen können, und schieben die untere Kante ein Stück nach oben. Als Nächstes ziehen Sie aus der Objekt-Bibliothek jeweils einen Button, einen Stepper und ein Label in den grauen Bereich unter den Textview. Buttons und Labels kennen Sie bereits aus dem ersten Kapitel. Ein Stepper ist das mittlere Element in der Abbildung; es hat die Klasse UIStepper und erlaubt die schrittweise Erhöhung oder Verringerung eines Wertes.

Abbildung

Abbildung 2.21 Erweiterung des Views um drei neue Elemente

Sie können den Wertebereich und die Schrittweite des Steppers über dessen Attributinspektor einstellen, den Sie über die Tastenkombination alt + cmd + 4 öffnen. Den Wertebereich legen Sie über die Felder Minimum und Maximum, die Schrittweite über Step und den aktuellen Wert über Current fest. Verwenden Sie als Einstellung dafür jeweils die in Abbildung 2.22 gezeigten Werte. Damit erhöht der Stepper seinen Wert um 1, wenn Sie seinen Plusbutton drücken, und verringert ihn entsprechend durch Drücken des Minusbuttons. Dabei bleibt der Wert allerdings immer zwischen 0 und 100.

Abbildung

Abbildung 2.22 Den Wertebereich und die Schrittweite des Steppers einstellen

Als Nächstes öffnen Sie den Hilfseditor und ziehen eine Verbindung vom Stepper in die Klassendeklaration des Viewcontrollers. In dem Popup wählen Sie unter Connection den Eintrag Action aus. Geben Sie der Action den Namen »updateModel«. Die anderen Einstellungen lassen Sie unverändert und drücken Connect. Xcode legt danach eine Deklaration und eine Definition für diese neue Methode an. Methoden, die wie updateModel: mit einem Eingabeelement verbunden sind beziehungsweise werden können, bezeichnet man auch als Actionmethoden.

Die Methodendefinition befindet sich natürlich in der Implementierungsdatei ViewController.m des Viewcontrollers. Die neue Methode soll nun die Anzahl der Droiden im Modell ändern, indem sie den Wert des Steppers als Parameter beim Aufruf der Methode updateDroids: des Models verwendet. Um an den Wert des Steppers zu kommen, könnten Sie ein Outlet anlegen, wie Sie es bereits für den Textview gemacht haben. Allerdings übergibt Ihnen Cocoa Touch praktischerweise diesen View auch als Parameterwert für die Methode updateModel:, sodass Sie den Wert über [sender value] ermitteln können.

Allerdings warnt Xcode Sie bei Verwendung dieses Ausdrucks, da der Parameter sender den Typ id hat, und es mehrere Klassen gibt, die eine Methode value deklarieren. Leider haben die Deklarationen unterschiedliche Rückgabetypen, sodass hier ein mehrdeutiger Ausdruck vorliegt. Sie können diesen Missstand jedoch leicht beheben, indem Sie in der Deklaration den Typ des Parameters auf UIStepper * ändern:

- (IBAction)updateModel:(UIStepper *)sender {
NSInteger theValue = [sender value];

[self.model updateDroids:theValue];
}

Listing 2.20 Übergabe des Stepper-Wertes an das Model

Sprechende Methodennamen

Der Methodenname updateModel: drückt schon relativ gut aus, was diese Methode macht, und er ist daher wesentlich besser als die gern verwendeten nichtssagenden Namen click: oder buttonClick:, die man in vielen Programmen sieht. Ein guter Methodenname sollte immer beschreiben, was die Methode macht. Sprechende Bezeichner erleichtern das Verständnis des Codes erheblich.

Da die Methode jetzt einen Stepper als Parameter erwartet, können Sie das gegebenenfalls auch noch im Namen ausdrücken und sie in updateModelWithStepper: umbenennen, da viele Actionmethoden mit unterschiedlichen Eingabeelementen funktionieren.

Analog dazu müssen Sie natürlich auch in der Methodendeklaration den Typ des Parameters anpassen. Wenn Sie nun den Stepper betätigen, bemerken Sie allerdings noch keine Auswirkungen Ihrer Änderungen. Deswegen fügen Sie zur Implementierung des Models noch die Methode countOfObjects hinzu:

- (NSInteger)countOfObjects {
return [objects count];
}

Listing 2.21 Anzahl der Objekte im Modell bereitstellen

Sie gibt die Anzahl der Objekte im Modell zurück, und Sie fügen in der Datei Model.h die entsprechende Methodendeklaration – (NSInteger)countOfObjects; hinzu. Durch diese Ergänzung können Sie nun auch im Viewcontroller auf diesen Wert zugreifen und ihn in der Actionmethode updateModel: ausgeben.

- (IBAction)updateModel:(UIStepper *)sender {
NSInteger theValue = [sender value];

[self.model updateDroids:theValue];
[self writeLog:[NSString stringWithFormat: Zeilenumbruch
@"countOfObjects = %d", [self.model countOfObjects]]];
}

Listing 2.22 Ausgabe der Objekte im Model

Nach der Erweiterung sehen Sie nach jedem Drücken eines Stepper-Buttons, wie viele Droiden sich im Modell befinden.


Galileo Computing - Zum Seitenanfang

2.2.4 VererbungZur nächsten ÜberschriftZur vorigen Überschrift

In der Einführung zur objektorientierten Programmierung in Abschnitt 2.1 haben Sie bereits das Paradigma des Vererbens kennengelernt. Implizit haben Sie Vererbung schon in der Beispiel-App verwendet, denn die Klasse ViewController im Projekt ist ja eine Subklasse von UIViewController, und die beiden von Ihnen selber zum Projekt hinzugefügten Klassen Model und Droid sind Subklassen von NSObject. In allen drei Headerdateien der Klassen sehen Sie in der Klassendeklaration jeweils die Superklasse der entsprechenden Klasse. Der Name der Superklasse steht dabei jeweils in der Zeile der @interface-Direktive, durch einen Doppelpunkt vom Namen der Klasse getrennt:

@interface ViewController : UIViewController
@interface Model : NSObject
@interface Droid : NSObject

Listing 2.23 Deklaration der Klassen mit Angabe der Superklassen

Um eine Klasse von einer bestehenden Klasse abzuleiten, müssen Sie also nichts weiter tun, als die Superklasse entsprechend im Interface anzugeben. Allerdings muss dafür die Deklaration der Superklasse vorliegen. Das geschieht über den Import der Headerdatei der Superklasse. Die Klasse UIViewController deklariert die Headerdatei UIKit/UIViewController.h, die die Datei ViewController.h über die Datei UIKit/UIKit.h importiert. Über diese Headerdatei importieren Sie alle Deklarationen des UIKit-Frameworks (oder auch kurz UIKits) von Cocoa Touch, deren Elemente Sie am Präfix »UI« erkennen.

Die Klasse NSObject gehört hingegen zum Foundation-Framework, und deshalb importieren Model.h und Droid.h jeweils die Datei Foundation/Foundation.h. Die Grundlagen dieses Frameworks stammen noch aus NeXTStep-Zeiten, weswegen seine Elemente das Präfix »NS« aufweisen.

Spitze Klammern oder Anführungszeichen?

Vielleicht ist Ihnen aufgefallen, dass diese Import-Anweisungen spitze Klammern anstatt Anführungszeichen verwenden. Darüber teilen Sie dem Compiler mit, dass er diese Dateien nicht im Projekt, sondern in den Systemverzeichnissen suchen soll. Die Headerdateien des Projekts binden Sie also mit Anführungszeichen ein, zum Beispiel:

#import <Foundation/Foundation.h>
#import "Model.h"

Als Nächstes fügen Sie dem Projekt eine neue Klasse mit einer Superklasse hinzu, die Sie selber festlegen. Dazu verwenden Sie allerdings keine Vorlage aus der Bibliothek, sondern rufen wieder das Xcode-Menü FileNewFile ... auf. Im ersten Dialogfenster wählen Sie in der linken Spalte den Punkt iOSCocoa Touch und danach im rechten Bereich die Vorlage Objective-C class (siehe Abbildung 2.23).

Abbildung

Abbildung 2.23 Eine neue Klasse im Projekt

Im folgenden Dialog geben Sie der Klasse den Namen ProtocolDroid [Neben schlechtem Humor zeichnen sich Informatiker durch eine Vorliebe für Star Wars aus.] und wählen als Superklasse die Klasse Droid aus, wie Abbildung 2.24 zeigt.

Abbildung

Abbildung 2.24 Eine Klasse vom Typ »Droid«

Nach Beendigung des Dialogs wählen Sie noch den Ablageort der Dateien, an dem Xcode danach die Interface- und die Headerdatei anlegt. In der Headerdatei sehen Sie, dass die neue Klasse die Superklasse Droid hat. Außerdem hat die IDE auch schon die Deklaration der Superklasse über die Headerdatei Droid.h importiert, die ja für die Deklaration von ProtocolDroid notwendig ist:

#import "Droid.h"
@interface ProtocolDroid : Droid
@end

Listing 2.24 Das Interface der Klasse »ProtocolDroid«

Anschließend erzeugen Sie eine weitere neue Klasse im Projekt, dieses Mal durch das Hinzufügen einer Objective-C class aus der File Template Library. Geben Sie der Klasse den Namen AstroDroid. Xcode öffnet hierbei lediglich ein Fenster, in dem Sie den Namen der Implementierungsdatei [Dies impliziert auch den Klassennamen.] angeben können. Den Namen der Superklasse können Sie hierbei jedoch nicht angeben. Die neue Klasse stammt deswegen immer von NSObject ab, und dementsprechend sieht die Headerdatei dieser Klasse zunächst wie folgt aus:

#import <Foundation/Foundation.h>
@interface AstroDroid : NSObject
@end

Listing 2.25 Das Interface der Klasse »AstroDroid« nach dem Anlegen

Wie der Name der Klasse schon vermuten lässt, soll auch sie eine Subklasse von Droid sein. Ändern Sie daher die Superklasse in der Deklaration von NSObject auf Droid, und ersetzen Sie die #import-Anweisung durch den Import von Droid.h: [Achten Sie dabei auch darauf, die spitzen Klammern durch Anführungszeichen zu ersetzen; Droid.h ist ja keine Headerdatei des Systems.]

#import "Droid.h"
@interface AstroDroid : Droid
@end

Listing 2.26 Die Klasse »AstroDroid« mit neuer Superklasse

Die Klasse ProtocolDroid wird zunächst keine Änderungen erfahren. Somit erbt sie alle Eigenschaften ihrer Superklasse, also von Droid. Um das Wesen der Vererbung von Eigenschaften zu zeigen, ändern Sie das Modell der Beispiel-App jetzt so, dass es neben Objekten vom Typ Droid auch Objekte vom Typ ProtocolDroid verwaltet. Fügen Sie dazu in der Implementierungsdatei Model.m die Anweisungen

#import "Model.h"
#import "Droid.h"
#import "ProtocolDroid.h"
#import "AstroDroid.h"

Listing 2.27 Erweiterung der Importanweisungen im Modell

hinzu, um der Implementierung der Klasse Model die Klassen ProtocolDroid und AstroDroid bekannt zu machen. Anschließend erweitern Sie die Methode updateModel: in dieser Datei um die hervorgehobenen Zeilen:

- (void)updateModel:(NSInteger)inValue {
if(inValue > [objects count]) {
NSInteger theRemainder = inValue % 3;
Droid *theDroid;

if(theRemainder == 0) {
theDroid = [[Droid alloc] initWithID:inValue];
}
else if(theRemainder == 1) {
theDroid = [[ProtocolDroid alloc] Zeilenumbruch
initWithID:inValue];
}
else {
theDroid = [[AstroDroid alloc] Zeilenumbruch
initWithID:inValue];
}
[objects addObject:theDroid];
}
else if (inValue < [objects count]) {
[objects removeLastObject];
}
}

Listing 2.28 Anlegen von unterschiedlichen Modellobjekten

Die Methode legt nun abwechselnd Droiden aller drei Arten an, wobei sie deren Typ anhand des Parameterwerts auswählt. Dabei berechnet der Operator % den Divisionsrest von zwei Zahlen. Bei positiven Werten für inValue können hierbei nur die Werte 0, 1 und 2 herauskommen, und anhand dieses Restes wählt die Methode die Klasse aus.

Die Implementierung nutzt außerdem zwei Vorteile der Objektorientierung aus. Bislang haben Sie den Initializer nur in der Klasse Droid implementiert. Die Methode verwendet ihn dennoch für die Erzeugung von Protokoll- und Astrodroiden. Das ist möglich, da ja deren Klassen das Verhalten der Klasse Droid erben und somit auch die Methode initWithID:. Des Weiteren weist die Methode alle Droiden der gleichen Variablen theDroid vom Typ Droid zu, was Ihnen das Subtyping erlaubt. AstroDroid und ProtocolDroid sind ja Subtypen von Droid, weswegen Sie deren Objekte auch überall dort verwenden dürfen, wo Sie Droiden verwenden. Ein Protokolldroide ist ja schließlich ein Droide.

Um die neue Funktionalität zu überprüfen, erweitern Sie auch die Methode initWithID: in der Datei Droid.m um die im folgenden Listing hervorgehobene NSLog-Anweisung:

- (id)initWithID:(NSInteger)inID {
self = [super init];
if(self != nil){
self.droidID = Zeilenumbruch
[NSString stringWithFormat:@"0xDEADBEEF%i", inID];
}
NSLog(@"[+] %@.%@", self, NSStringFromSelector(_cmd));
return self;
}

Listing 2.29 Erweiterung der Initialisierung um eine Log-Anweisung

Damit können Sie dann in der Konsolenausgabe sehen, für welche Klassen das Modell die Methode initWithID: aufgerufen hat, da die Log-Anweisung für self jeweils den Klassennamen und die Speicherdresse des Objekts ausgibt. Wenn Sie die App übersetzen, starten und neue Objekte über den Plusbutton des Steppers einfügen, können Sie in der Konsole sehen, dass das Modell Droiden der Klassen Droid, ProtocolDroid und AstroDroid in dieser Reihenfolge einfügt. Dazwischen steht jeweils noch eine weitere Zeile, die der Aufruf von writeLog: in der Methode updateModel: des Viewcontrollers verursacht:

[+] <ViewController: 0x71619e0>.writeLog:
[+] <ProtocolDroid: 0x753d0c0>.initWithID:
[+] <ViewController: 0x71619e0>.writeLog:
[+] <AstroDroid: 0x753fed0>.initWithID:
[+] <ViewController: 0x71619e0>.writeLog:
[+] <Droid: 0x7585900>.initWithID:
[+] <ViewController: 0x71619e0>.writeLog:
[+] <ProtocolDroid: 0x75907b0>.initWithID:
[+] <ViewController: 0x71619e0>.writeLog:
[+] <AstroDroid: 0x75894f0>.initWithID:
[+] <ViewController: 0x71619e0>.writeLog:

Listing 2.30 Konsolenausgabe beim Einfügen von Droiden

Abbildung 2.25 stellt die Aufrufe der Initialisierungsmethoden noch einmal grafisch für die einzelnen Klassen dar. Wenn das Modell beispielsweise ein Objekt der Klasse Droid initialisiert, sendet es die Nachricht initWithID: an das Objekt. Die Methode sendet wiederum die Nachricht init, die zur Ausführung der init-Methode in NS-Object führt. Wenn das Modell die initWithID:-Nachricht an einen Protokolldroiden sendet, ruft die Laufzeitumgebung den entsprechenden Initializer der Superklasse Droid auf, da die Klasse ProtocolDroid diese Methode ja nicht implementiert. [Das ist in der Abbildung durch die graue Schriftfarbe angedeutet.] Die Klasse AstroDroid implementiert die initWithID:-Methode, weswegen diese Klasse die entsprechende Nachricht verarbeitet. Sie sendet ihrerseits die init-Nachricht, wobei die Laufzeitumgebung wegen des Empfängers super jedoch nur die Superklassen von AstroDroid, also Droid und NSObject, nach Implementierungen durchsucht. Da nur die Klasse NSObject eine Implementierung bereitstellt, führt diese Nachricht zum Aufruf dieser Methode aus NSObject.

Kopieren Sie nun die Implementierung der Methode initWithID: aus der Klasse Model in den Implementierungsblock der Datei AstroDroid.m, und verändern Sie den Formatstring für die Erzeugung der Zeichenkette für die Property droidID:

- (id)initWithID:(NSInteger)inID {
self = [super init];
if(self != nil){
self.droidID = Zeilenumbruch
[NSString stringWithFormat:@"0xBEEFCAFE%i", inID];
}
NSLog(@"[+] %@.%@", self, NSStringFromSelector(_cmd));
return self;
}

Listing 2.31 Astrodroiden sind anders als andere Droiden.

Abbildung

Abbildung 2.25 Aufruf der Methoden über die Vererbungshierarchie

Damit Sie die neuen Kennungen auch zu sehen bekommen, fügen Sie zunächst die Methode listDroids zu der Klasse Model hinzu, wobei Sie auch die entsprechende Deklaration – (void)listDroids; in die Klassendeklaration einfügen müssen.

- (void)listDroids {
NSLog(@"[+] Current droids (%d):", [self countOfObjects]);
for(Droid *aDroid in objects) {
NSLog(@"[+] %@: %@", aDroid, aDroid.droidID);
}
}

Listing 2.32 Ausgabe aller Droiden in der Konsole

Auch bei dieser Schleife nutzen Sie wieder aus, dass die Klassen aller Objekte in dem Array von der Klasse Droid erben. Dadurch können Sie an jedes Element die Nachricht droidID senden, wobei das Listing hier die Punktnotation (also die alternative Schreibweise aDroid.droidID für Accessoren) anstelle von [aDroid droidID] verwendet.

Fast Enumeration

Listing 2.32 verwendet zur Ausgabe aller Droiden auf die Konsole eine spezielle for-Schleife, die Apple als Fast Enumeration bezeichnet. Sie verwendet keine Indexvariable und keinen indizierten Elementzugriff, sondern weist die Elemente direkt der Schleifenvariablen zu. Die dem entsprechende klassische C-Schleife sieht so aus:

int theCount = [objects count];

for(int i = 0; i < theCount; ++i) {
Droid *aDroid = [objects objectAtIndex:i];

NSLog(@"[+] %@: %@", aDroid, aDroid.droidID);
}

Danach erstellen Sie eine Actionmethode für den Button, den Sie nachträglich zum View hinzugefügt haben (siehe Abbildung 2.21). Diesmal erstellen Sie zuerst die Actionmethode, indem Sie die Methode listModel: in die Datei ViewController.m einfügen. Eine Deklaration der Methode in der Headerdatei ViewController.h ist nicht erforderlich, da Sie diese Methode nicht aus einer anderen, von Ihnen erstellten Klasse aufrufen.

- (IBAction)listModel:(id)sender {
[self.model listDroids];
}

Listing 2.33 Actionmethode mal ohne Hilfseditor

Abbildung

Abbildung 2.26 Verbindung zum Viewcontroller über den »File’s Owner«

Diese Methode müssen Sie jetzt noch mit dem Button verbinden. Dazu öffnen Sie die Datei ViewController.xib und ziehen bei gedrückter ctrl-Taste eine Verbindung vom Button zu dem Files Owner in der Objektleiste der Datei, wie Abbildung 2.26 zeigt. Der Files Owner repräsentiert dabei das Viewcontroller-Objekt in der XIB-Datei.

Nach dem Loslassen öffnet sich ein Popupmenü, indem Sie die soeben erstellte Actionmethode finden; wählen Sie diesen Eintrag aus. Der Button ist nun mit der Methode verbunden, und wenn Sie nach dem Start der App zunächst einige Male den Plusbutton des Steppers drücken und anschließend über den Button die Actionmethode auslösen, sollten Sie in der Konsole alle Droiden mit deren Kennungen sehen (siehe Listing 2.34).

[+] Current droids (6):
[+] <ProtocolDroid: 0x7181260>: 0xDEADBEEF1
[+] <AstroDroid: 0x759deb0>: 0xBEEFCAFE2
[+] <Droid: 0x71e1a30>: 0xDEADBEEF3
[+] <ProtocolDroid: 0x759f3a0>: 0xDEADBEEF4
[+] <AstroDroid: 0x759f040>: 0xBEEFCAFE5

Listing 2.34 Ausgabe der Droiden mit Kennung

Abbildung

Abbildung 2.27 Actionverbindung erstellen

Actionmethoden

Bei der Erstellung dieser Action haben Sie zwei Dinge gesehen: Zum einen lassen sich Actionmethoden auch ohne Hilfseditor wie gewöhnliche Methoden erstellen und über den Interface Builder mit dem View verbinden. Zum anderen ist es nicht notwendig, dass Sie Actionmethoden in der Headerdatei deklarieren. Wenn Sie also eine Actionmethode vor dem direkten Zugriff aus anderen Objekten schützen möchten, können Sie sie auf diesem Weg verstecken.

Die Schleife versendet die Nachricht droidID ja an Objekte mit unterschiedlichem Typ. Das funktioniert hier, weil sich die entsprechende Methode in der gemeinsamen Superklasse Droid befindet. Es reicht in Objective-C im Gegensatz zu vielen anderen Programmiersprachen allerdings vollkommen aus, dass die Objekte die entsprechende Methode besitzen. Sie müssen nicht unbedingt eine gemeinsame Superklasse haben.

Um dies zu verdeutlichen, fügen Sie der Beispiel-App noch eine weitere Klasse mit der Superklasse NSObject hinzu. Ob Sie die Klasse über das Menü oder die File Template Library hinzufügen, ist Ihnen überlassen. Geben Sie der Klasse den Namen Wookiee, und fügen Sie eine Property name vom Typ NSString, eine Methode sayName und eine Methode initWithName: hinzu, sodass Sie die folgende Klassendeklaration erhalten:

@interface Wookiee : NSObject

@property(copy) NSString *name;

-(id)initWithName:(NSString *)inName;
-(void)sayName;

@end

Listing 2.35 Jetzt kommen Wookiees ins Spiel.

In der Implementierung synthetisieren Sie zunächst die Property und fügen anschließend die beiden Methoden ein. Die Methode initWithName: entspricht der Methode initWithID: aus der Klasse Droid. Die Methode sayName schreibt den Wert der Property name in die Konsole, gemeinsam mit dem aktuellen Objekt- und dem Methodennamen.

#import "Wookiee.h"

@implementation Wookiee

@synthesize name;

- (id)initWithName:(NSString *)inName {
self = [super init];
if(self != nil){
self.name = inName;
}
NSLog(@"[+] %@.%@", self, NSStringFromSelector(_cmd));
return self;
}

-(void)sayName{
NSLog(@"[+] %@.%@: %@", self, NSStringFromSelector(_cmd), Zeilenumbruch
self.name);
}

@end

Listing 2.36 Implementierung der Klasse »Wookiee«

Aber nicht nur Wookiees, sondern auch die Droiden sollen ihren Namen ausgeben können. Fügen Sie deshalb die Methodendeklaration – (void)sayName; in die Headerdatei Droid.h ein, und implementieren Sie die Methode in Droid.m folgendermaßen:

-(void)sayName{
NSLog(@"[+] %@.%@: %@", self, NSStringFromSelector(_cmd), Zeilenumbruch
self.droidID);
}

Listing 2.37 Auch Droiden sagen ihren Namen.

Diese Implementierung kopieren Sie auch in den Implementierungsblock der Klasse AstroDroid und ersetzen das Pluszeichen am Anfang durch ein Sternchen. Danach importieren Sie in Model.m die Deklaration der Wookiee-Klasse durch #import "Wookiee.h" und passen die Implementierung von listDroids so an, wie in Listing 2.38 gezeigt wird:

- (void)listDroids {
Wookiee *theWookiee =
[[Wookiee alloc] initWithName:@"Chewbacca"];

[objects addObject:theWookiee];

NSLog(@"[+] Current droids (%d):", [self countOfObjects]);
for(Droid *aDroid in objects) {
[aDroid sayName];
}
}

Listing 2.38 Wookiees mischen die Droiden auf.

Jeder Aufruf dieser Methode fügt nun ein Objekt der Klasse Wookiee mit dem Namen »Chewbacca« zu dem Array objects hinzu. Danach durchläuft die Schleife wieder alle Objekte des Arrays und sendet an jedes die Nachricht sayName. Die Konsolenausgabe des Programms für die Schleife enthält nun auch Wookiees:

[+] <Wookiee: 0x7149a20>.initWithName:
[+] Current droids (5):
[+] <ProtocolDroid: 0x71b3b30>.sayName: 0xDEADBEEF1
[*] <AstroDroid: 0x71a0660>.sayName: 0xBEEFCAFE2
[+] <Droid: 0x71c6820>.sayName: 0xDEADBEEF3
[+] <ProtocolDroid: 0x719cdf0>.sayName: 0xDEADBEEF4
[+] <Wookiee: 0x7149a20>.sayName: Chewbacca

Listing 2.39 Ein Wookiee unter Droiden

Dabei fällt auf, dass die Schleifenvariable aus Listing 2.38 zwar den Klassennamen Droid verwendet, sie anscheinend aber auch klaglos mit Objekten der Klasse Wookiee zurechtkommt, obwohl ein Wookiee kein Droide ist. Das liegt daran, dass die Laufzeitumgebung nicht prüft, ob der Variablentyp mit der Klasse des Objekts zusammenpasst. Wenn der Wookiee in der Schleife an der Reihe ist, zeigt also aDroid auf ein Objekt der Klasse Wookiee, und niemanden scheint es zu stören. Sogar der Methodenaufruf funktioniert, da ja sowohl die Droiden als auch die Wookiees die Methode sayName implementieren.

Es ist allerdings ein sehr schlechter Stil, Variablen für Objekte mit einem unpassenden Typ zu verwenden, und dem guten Chewie würde das sicherlich auch nicht gefallen. Es ist besser, hier einen passenden Typ für die Variable zu verwenden.

Die Klassen Droid und Wookiee haben die gemeinsame Superklasse NSObject. Wenn Sie die Deklaration der Schleifenvariable allerdings in NSObject *aDroid ändern, erhalten Sie für die Anweisung [aDroid sayName] die Fehlermeldung, da ja die Klasse NSObject nicht die Methode sayName implementiert. Besser ist hier die Verwendung des Typs id für die Variable, da Sie an Variablen dieses Typs jede Nachricht senden können, solange sie irgendeine bekannte Klasse deklariert. Ändern Sie also den Typ in id – und am besten auch gleich den Variablennamen in anItem, um den mächtigen Chewbacca nicht zu verärgern:

for(id anItem in objects) {
[anItem sayName];
}

Listing 2.40 Wookiee-freundliche Version der Schleife

Nachrichten versenden und verarbeiten

Wie Sie an dem Beispiel mit dem Wookiee gesehen haben, ist bei Objective-C die Klasse nicht so wichtig wie in vielen anderen Programmiersprachen. Durch diese Trennung von Nachrichtenversand und -verarbeitung ist Objective-C sehr dynamisch, da Sie bei der Implementierung nicht mehr darauf angewiesen sind, ob die Objekte bestimmte Klassen besitzen. Es reicht vollkommen aus, wenn sie über die notwendigen Methoden verfügen.


Galileo Computing - Zum Seitenanfang

2.2.5 KategorienZur nächsten ÜberschriftZur vorigen Überschrift

Eng mit der Vererbung verwandt sind Kategorien. Angenommen, Sie möchten eine bestehende Klasse um eine oder mehrere Methoden erweitern, die Ihnen die Arbeit mit dieser Klasse erleichtern, dann können Sie eine Subklasse erzeugen und die neue Methode in der Subklasse verwenden. Das ist allerdings bei bereits bestehenden Objekten nicht möglich, da Sie ja nicht die Klasse eines Objekts austauschen können. An dieser Stelle helfen Kategorien, über die Sie Klassen nachträglich erweitern können, ohne von ihnen ableiten zu müssen.

Mit Kategorien können Sie aber nicht nur die selbst geschriebenen Klassen erweitern, sondern auch solche, die schon fertig vorliegen und deren Implementation Sie nicht kennen. Obendrein vererben sich ihre Methoden auch auf die Subklassen der Klasse. Sie können beispielsweise mit einer Kategorie der Klasse NSObject neue Methoden hinzufügen, und alle Subklassen erben diese neuen Methoden. Damit steht dann diese neue Methode fast allen Klassen zur Verfügung.

Als Beispiel für eine Kategorie soll die Klasse NSString die Methode reversedString erhalten, die die enthaltene Zeichenkette umgedreht zurückgibt. Eine Kategorie besitzt, wie eine Klasse, eine eigene Header- und Implementierungsdatei. Fügen Sie daher eine neue Kategorie über FileNewFile ... zu dem vorhandenen Xcode-Projekt hinzu, indem Sie die Vorlage Objective-C category verwenden (siehe Abbildung 2.28).

Abbildung

Abbildung 2.28 Vorlage für Kategorien auswählen

Im folgenden Dialog geben Sie in das Feld Category den Namen »ReverseString« und unter Category on den Klassennamen »NSString« ein, wie Abbildung 2.29 zeigt.

Abbildung

Abbildung 2.29 Eingabe des Kategorienamens und der Klasse

Xcode fügt nach erfolgreicher Beendigung des Wizzards die beiden Dateien NS-String+ReverseString.h und NSString+ReverseString.m in Ihr Projekt ein. Die Schnittstelle der Kategorie sieht dabei einer Klassendeklaration sehr ähnlich; anstelle des Doppelpunktes und der Superklasse steht hier allerdings der Name der Kategorie in runden Klammern. Die Vorlage bindet außerdem die Headerdatei des Foundation-Frameworks mit ein, da für eine Kategoriedeklaration immer die Deklaration der Klasse bekannt sein muss, auf der sie basiert.

#import <Foundation/Foundation.h>

@interface NSString (ReverseString)

@end

Listing 2.41 Headerdatei einer Kategorie

Analog sieht die Implementierung der Kategorie aus:

#import "NSString+ReverseString.h"

@implementation NSString (ReverseString)

@end

Listing 2.42 Implementierung einer Kategorie

In der Deklaration fügen Sie nun die Deklaration für die Methode reversedString hinzu:

@interface NSString (ReverseString)

-(NSString *)reversedString;

@end

Listing 2.43 Methodendeklaration in einer Kategorie

Diese Methode können Sie danach implementieren. Allerdings benötigen Sie dafür eine Subklasse von NSString, die Sie bislang noch nicht verwendet haben. Im Gegensatz zu NSString erlaubt NSMutableString nämlich die Änderung der enthaltenen Zeichenkette nach der Initialisierung.

- (NSString *)reversedString {
int theLength = [self length];
NSMutableString *theReverse = Zeilenumbruch
[[NSMutableString alloc] init];

for(int i = theLength – 1; i>=0; i--){
[theReverse appendFormat:@"%C", Zeilenumbruch
[self characterAtIndex:i]];
}
return theReverse;
}

Listing 2.44 Implementierung der Kategorie-Methode

Da die Kategorie eine Erweiterung der Klasse NSString ist, enthält self eine Referenz auf ein Objekt dieser Klasse, und über [self length] erhalten Sie folglich die Anzahl der enthaltenen Zeichen. Die for-Schleife geht danach die Zeichen in der Zeichenkette von hinten nach vorne durch und hängt sie an eine solche veränderliche Zeichenkette an, die sie schließlich als Ergebnis liefert. Das ist möglich, da ja NSMutableString eine Subklasse von NSString ist.

Die neue Kategoriemethode soll nun bei den Protokolldroiden zum Einsatz kommen, die ja von Haus aus immer einen etwas verwirrten Eindruck machen. Deswegen soll die Propertymethode droidID ihre Kennung verkehrt herum zurückgeben. Dafür müssen Sie die Headerdatei der Kategorie einbinden und die Methode droidID überschreiben. Auch wenn Sie in der Superklasse Droid nicht explizit eine Methode droidID geschrieben haben, können Sie sie dennoch überschreiben und auch per super-Aufruf darauf zugreifen:

#import "ProtocolDroid.h"
#import "NSString+ReverseString.h"

@implementation ProtocolDroid

- (NSString *)droidID {
return [[super droidID] reversedString];
}

@end

Listing 2.45 Verwendung der Kategoriemethode

Durch diese Änderung ändert sich dann die Konsolenausgabe für Protokolldroiden:

[+] <ProtocolDroid: 0x7528910>.sayName: 1FEEBDAEDx0
[*] <AstroDroid: 0x7523e80>.sayName: 0xBEEFCAFE2
[+] <Droid: 0x7125650>.sayName: 0xDEADBEEF3
[+] <ProtocolDroid: 0x7523e40>.sayName: 4FEEBDAEDx0
[+] <Wookiee: 0x7528f70>.sayName: Chewbacca

Listing 2.46 Protokolldroiden geben ihre Kennung verkehrt herum aus.

Kategorien versus Subklassen

Sie sollten jedoch Kategorien nicht als Ersatz für Subklassen verwenden. Das kann zu einem unerwarteten Verhalten Ihres Codes führen. Bestehende Methoden überschreiben Sie immer über Subklassen und nicht über Kategorien.

Mit der anonymen Kategorie gibt es noch einen Sonderfall bei den Kategorien. Wie der Name schon andeutet, hat eine anonyme Kategorie keinen Namen. Stattdessen geben Sie bei der Deklaration nur ein leeres Klammerpaar an.

@interface Droid()

@end

Listing 2.47 Deklaration einer anonymen Kategorie

Die Implementierung ihrer Methoden erfolgt hier nicht in einem entsprechenden Implementierungsblock, sondern in der Implementierung der Klasse. Auch anonyme Kategorien haben den Zweck, ihre Klassen um Methoden zu erweitern. Im Gegensatz zu den benannten Kategorien sollen deren Methoden aber nicht öffentlich zugänglich sein. Sie können über die anonyme Kategorie also private Methoden einer Klasse deklarieren. Außerdem gestatten anonyme Kategorien die Deklaration von synthetisierten Propertys, die dadurch ebenfalls privat sind. Aus diesem Grund erfolgt die Kategorie-Deklaration auch in der Implementierungsdatei der Klasse. Sie können damit beispielsweise das Attribut objects in der Klasse Model durch eine Property ersetzen, die für andere Klassen nicht sichtbar ist:

@interface Model()

@property (strong) NSMutableArray *objects;

@end

@implementation Model

@synthesize ;
@synthesize name;
@synthesize creation;
@synthesize objects;

- (id)init {
self = [super init];
if (self) {
self.creation = [NSDate date];
self.objects
= [[NSMutableArray alloc] init];
}
return self;
}

@end

Listing 2.48 Verwendung einer privaten Property im Model


Galileo Computing - Zum Seitenanfang

2.2.6 ProtokolleZur nächsten ÜberschriftZur vorigen Überschrift

Ein weiteres wichtiges Sprachmerkmal von Objective-C sind Protokolle. Protokolle bündeln Methoden, die nicht zu einer bestimmten Klasse gehören. Damit lässt sich die vermeintliche Beschränkung der fehlenden Mehrfachvererbung in Objective-C kompensieren. Um einer Klasse mehr vordeklarierte Methoden als nur von einer Superklasse hinzuzufügen, kann die Klasse über ein Protokoll weitere Methoden adaptieren. Protokolle eignen sich außerdem dazu, bestimmte Methoden bei einem Objekt vorauszusetzen, ohne dessen Klasse vorgeben zu müssen. Im Gegensatz zu Klassen und Kategorien bestehen sie jedoch immer nur aus einer Deklaration. Die Implementierung erfolgt immer in den adaptierenden Klassen.

Das Schlüsselwort @protocol kennzeichnet eine Protokolldeklaration. Sie dürfen auch Protokolle voneinander ableiten, indem Sie den Namen der Oberprotokolle in einer spitzen Klammer an den Protokollnamen in der Deklaration hängen. Das Foundation-Framework enthält neben der Klasse NSObject auch ein Protokoll mit dem gleichen Namen, das viele Methoden der Klasse deklariert. Sie sollten in der Regel dieses Protokoll als Oberprotokoll verwenden, sofern Sie keine anderen Protokolle dafür vorsehen.

Wenn Sie beispielsweise eine einheitliche Methode zum Laden von Daten für Ihre Klassen haben möchten, können Sie das über ein Protokoll beschreiben:

@protocol Loadable<NSObject>
@required
- (void)loadFromFile:(NSString *)inPath;
@optional
- (void)loadFromURL:(NSURL *)inURL;
@end

Listing 2.49 Deklaration eines Protokolls

Die Protokollmethoden können Sie dabei als zwingend erforderlich (@required) oder als optional (@optional) deklarieren. Erforderliche Methoden muss jede adaptierende Klasse implementieren, während Sie sich das bei optionalen Methoden aussuchen können. Jede Klasse, die das Beispielprotokoll Loadable adaptiert, muss also die Methode loadFromFile: implementieren. Standardmäßig sind alle Protokollmethoden erforderlich, wenn Sie diese Angaben weglassen.

Sie verwenden ein Protokoll, indem Sie es in spitzen Klammern hinter der Superklasse in der Klassendeklaration angeben.

@interface Droid : NSObject<Loadable> 

@end

Listing 2.50 Adaption eines Protokolls durch eine Klasse

Sie können natürlich auch mehrere Protokolle in einer Klasse implementieren. In diesem Fall trennen Sie die Namen der Protokolle in den spitzen Klammern einfach durch jeweils ein Komma. Es ist auch möglich, Protokolle über Kategorien zu implementieren. Hierfür platzieren Sie die spitzen Klammern einfach hinter den runden Klammern in der Kategorie-Deklaration:

@interface Droid(Loading)<Loadable>

@end

Listing 2.51 Adaption eines Protokolls über eine Kategorie

Das funktioniert natürlich auch mit mehreren Protokollen und auch bei anonymen Kategorien.


Galileo Computing - Zum Seitenanfang

2.2.7 VorwärtsdeklarationenZur nächsten ÜberschriftZur vorigen Überschrift

Viele Protokolldeklarationen befinden sich in den Headerdateien von Klassen, weil sie mit den entsprechenden Klassen eng verwoben sind. Dabei entsteht häufig das Problem, dass die Klasse das Protokoll und das Protokoll die Klasse kennen muss. Diese zyklische Abhängigkeit kann auch zwischen Klassen entstehen, beispielsweise wenn jeder Droide auf einen Protokolldroiden verweisen soll:

@interface Droid : NSObject

@property(copy) NSString *droidID;
@property(weak) ProtocolDroid *protocolDroid;

-(id)initWithID:(NSInteger)inDroidID;
-(void)sayName;

@end

Listing 2.52 Zyklische Abhängigkeit von Klassen

Bei der Klassendeklaration in Listing 2.52 verweist jeder Droide auf einen Protokolldroiden, dessen Klasse ihrerseits von Droid abgeleitet ist. Diese Beziehungen zwischen den zwei Klassen stellt auch Abbildung 2.30 grafisch dar. Dabei ist ja die Vererbungsbeziehung von der Klasse ProtocolDroid zu Droid unproblematisch, da Sie sie ja bereits implementiert und getestet haben. Wenn Sie nun jedoch versuchen, über die An-weisung #import "ProtocolDroid.h" die Klasse ProtocolDroid in der Headerdatei Droid.h bekannt zu machen, erhalten Sie spätestens beim Übersetzen des Projekts diese Fehlermeldung:

cannot find interface declaration for 'Droid', superclass of 'ProtocolDroid'

Abbildung

Abbildung 2.30 Zyklische Abhängigkeit zwischen zwei Klassen

Der Compiler kann die Klassen eben nur nacheinander verarbeiten, und so kommt er bei dieser zyklischen Referenz durcheinander. Dieses Dilemma lässt sich also nicht durch eine weitere Importanweisung auflösen. Die Lösung besteht nun in der Tatsache, dass nicht beide Klassen voneinander erben (können) und die Klasse Droid nicht die komplette Deklaration der Klasse ProtocolDroid kennen muss. Eine Vorwärtsdeklaration für ProtocolDroid vor der Klassendeklaration von Droid reicht hier aus:

@class ProtocolDroid;

@implementation Droid : NSObject

Listing 2.53 Vorwärtsdeklaration für die Klasse ProtocolDroid

Über das Schlüsselwort @class, gefolgt von einem Klassennamen, teilen Sie dem Compiler mit, dass es die Klasse gibt, dass aber ihr genaues Aussehen noch nicht bekannt ist. Analog können Sie über das Schlüsselwort @protocol Vorwärtsdeklarationen für Protokolle angeben:

@protocol Loadable;

Listing 2.54 Vorwärtsdeklaration für ein Protokoll

Vorwärtsdeklarationen eignen sich allerdings nur für Klassen und Protokolle, die Sie in Attribut-, Property- oder Methodendeklarationen verwenden. Für Vererbungsbeziehungen in Klassen-, Protokoll- und Kategoriedeklarationen müssen Sie hingegen immer die entsprechende Deklaration importieren.


Galileo Computing - Zum Seitenanfang

2.2.8 Kommunikation zwischen den SchichtenZur nächsten ÜberschriftZur vorigen Überschrift

Bei größeren Applikationen können mehrere Controller das Modell verwalten. Dabei kann es vorkommen, dass ein Controller Teile des Modells ändert, die ein anderer Controller gerade anzeigt. Letzterer kann natürlich ständig prüfen, ob dieser Fall eingetreten ist, um so die Anzeige immer aktuell zu halten. Auch wenn das Nachfragen eine höfliche Kommunikationsform ist, gehört es in der Programmierung nicht zum guten Ton. Zum einen führt ständiges Fragen, auch Polling genannt, zu einer unnötigen Systembelastung, und zum anderen ist – je nach Nachfrageintervall – eine zeitnahe Informationsübermittlung nicht sichergestellt. Findet eine Zustandsänderung im Modell zwischen zwei Intervallen des Controllers statt, bleibt die Änderung bis zur nächsten Abfrage unbemerkt.

In Cocoa Touch ermöglicht es das Key-Value-Observing (KVO), dass ein Objekt automatisch andere Objekte bei Zustandsänderungen benachrichtigt. Das ist die Umsetzung des Beobachtermusters. Das beobachtete Objekt informiert seinen Beobachter dann selbstständig und unmittelbar, wenn sich sein Zustand ändert.

Durch KVO können Sie es also dem Controller ermöglichen, sich über Zustandsänderungen im Modell informieren zu lassen. Dabei braucht das Modell sogar nur zu wissen, dass es Objekte gibt, die sich für die Zustandsänderung interessieren, aber nicht, welche Klassen sie haben. Und so erzeugt das Beobachtermuster dabei keine Abhängigkeit des Modells vom Controller, da die Modellobjekte die Klassen ihrer Beobachter nicht kennen. Diese Verbindung nennt man auch lose Kopplung.

Lose Kopplung

Die lose Kopplung des Modells an den Controller hat die schöne Eigenschaft, dass Sie das Modell auch ohne den Controller verwenden können. Im besten Fall können Sie es beispielsweise in eine App oder ein Kommandozeilenprogramm für den Mac einbauen, oder Sie schreiben automatisierte Testfälle, die prüfen, ob sich Ihr Modell auch wie vorgesehen verhält.

KVO ist allerdings nicht nur auf Modell- und Controllerklassen beschränkt, sondern Sie können es für beliebige Klassen mit der Superklasse NSObject verwenden, oder für Klassen die das Protokoll NSKeyValueObserving implementieren.

Über die Methode addObserver:forKeyPath:options:context: können Sie einen neuen Beobachter mit einem Zugriffspfad registrieren und ihn über removeObserver:forKeyPath: wieder abmelden. Wenn der Viewcontroller des Projekts beispielsweise den Wert der Property des Modells beobachten soll, können Sie ihn wie in Listing 2.55 als Beobachter registrieren. Es ist übrigens in vielen Fällen so, dass Objekte sich selber als Beobachter an- und abmelden:

[self.model addObserver:self forKeyPath:@""
options:NSKeyValueObservingOptionNew | Zeilenumbruch
NSKeyValueObservingOptionOld Zeilenumbruch
context:NULL];

Listing 2.55 Registrierung eines Beobachters

Nach dieser Registrierung benachrichtigt das Modell den Beobachter bei jeder Änderung des Wertes der Property. Die beiden Konstanten für den Optionsparameter geben dabei an, dass die Benachrichtigung immer den alten und den neuen Wert enthalten soll. Für den Empfang der Benachrichtigung muss nun der Empfänger die Methode observeValueForKeyPath:ofObject:change:context: implementieren.

Die Attributwerte enthält dabei das Dictionary change. Sie können die Methode folgendermaßen implementieren:

- (void) observeValueForKeyPath:(NSString *)inKeyPath Zeilenumbruch
ofObject:(id)inObject change:(NSDictionary *)inChange #zonk
context:(void *)inContext {
if([inKeyPath isEqualToString:@""]) {
NSLog(@"[+] Old status:%@", Zeilenumbruch
[inChange valueForKey:NSKeyValueChangeOldKey]);
NSLog(@"[+] New status:%@", Zeilenumbruch
[inChange valueForKey:NSKeyValueChangeNewKey]);

}
}

Listing 2.56 Implementierung einer Observierungsmethode

Die Registrierung aus Listing 2.55 registriert den Beobachter nur für genau ein Objekt-Attribut-Paar. Wenn Sie weitere Objekte oder Attribute beobachten möchten, müssen Sie diese gesondert registrieren. Vor der endgültigen Freigabe eines Beobachters müssen Sie ihn immer abmelden. Ansonsten erhalten Sie einen Laufzeitfehler, und Ihr Programm stürzt ab. Für dieses Beispiel sieht der entsprechende Aufruf folgendermaßen aus:

[self.model removeObserver:self forKeyPath:@""];

Listing 2.57 Abmelden eines Beobachters

Um Key-Value-Observing auszuprobieren, kopieren Sie die Methode aus Listing 2.56 in die Implementierung der Klasse ViewController und fügen die Registrierung aus Listing 2.55 in die Methode viewDidAppear: ein.

- (void)viewDidAppear:(BOOL)inAnimated {
[super viewDidAppear:inAnimated];
[self.model addObserver:self forKeyPath:@"status" Zeilenumbruch
options:NSKeyValueObservingOptionNew | Zeilenumbruch
NSKeyValueObservingOptionOld
context:NULL];
}

Listing 2.58 Der Viewcontroller registriert sich als Beobachter ...

Außerdem sollte sich der Viewcontroller auch in der Methode viewWillDisapper: wieder als Beobachter des Modells abmelden:

- (void)viewWillDisappear:(BOOL)inAnimated {
[self.model removeObserver:self forKeyPath:@""];
[super viewWillDisappear:inAnimated];
}

Listing 2.59 ... und meldet sich auch brav wieder ab.

Damit nun der Beobachter zum Zuge kommt, muss das Model den Wert seiner Property verändern. Dazu fügen Sie am Ende des ersten if-Blocks in der Methode updateDroids: der Klasse Model die hervorgehobene Zeile aus Listing 2.60 ein.

else {
theDroid = [[AstroDroid alloc] initWithID:inValue];
}
self.status = theDroid.droidID;

[self.objects addObject:theDroid];

Listing 2.60 Änderung im Modell

Wenn Sie nun nach dem Start der App zweimal den Plusbutton des Steppers drücken, finden Sie folgende Einträge in der Konsolenausgabe:

[+] <ProtocolDroid: 0x719c8d0>.initWithID:
[+] Old status:<null>
[+] New status:1FEEBDAEDx0
[+] <ViewController: 0x718d850>.writeLog:
[+] <AstroDroid: 0x7187220>.initWithID:
[+] Old status:1FEEBDAEDx0
[+] New status:0xBEEFCAFE2
[+] <ViewController: 0x718d850>.writeLog:

Listing 2.61 Ausgabe der Statusänderungen durch KVO

Jedes Mal, wenn Sie der Property status einen neuen Wert zuweisen, benachrichtigt das Modell jetzt den Viewcontroller und teilt ihm mit, was sich geändert hat. Sie haben mit KVO also ein mächtiges und elegantes Werkzeug, um Änderungen am Modell mitzubekommen. Außerdem vermeiden Sie darüber auch Abhängigkeiten vom Modell zum Controller, da das Modell ja dessen Klassen nicht kennt.

Aber KVO kann noch mehr: Sie können damit auch Werte beobachten, die es nicht gibt. Um das zu illustrieren, soll das Label neben dem Stepper die Anzahl der Objekte im Modell anzeigen. Die Xcode-Vorlage für die Viewcontroller-Klasse hat bereits eine anonyme Kategorie für die Klasse angelegt. Dort fügen Sie die Deklaration der Property countLabel hinzu, und in den Implementierungsblock fügen Sie die entsprechende @synthesize-Anweisung ein:

@interface ViewController()

@property (strong, nonatomic) IBOutlet UILabel *countLabel;

@end

@implementation ViewController

@synthesize countLabel;

@synthesize stepper;

Listing 2.62 Deklaration einer privaten Property

Durch den Bezeichner IBOutlet vor dem Klassennamen UILabel erlauben Sie dem Interface Builder, diese Property mit einem Label im View zu verbinden. Öffnen Sie also den Interface Builder, und öffnen Sie dann den Verbindungsinspektor des File’s Owner, indem Sie ihn auswählen und die Tastenkombination alt + cmd + 6 drücken. In dem Inspektor finden Sie den Eintrag countLabel mit einem leeren Kreis rechts daneben. Klicken Sie mit der Maus in den Kreis, und ziehen Sie eine Verbindung zum Label auf dem View, wie das Abbildung 2.31 zeigt.

Als Nächstes registrieren Sie einen weiteren Beobachter auf den Schlüssel «objects« beim Modell und melden ihn auch entsprechend ab:

- (void)viewDidAppear:(BOOL)inAnimated {
[super viewDidAppear:inAnimated];
[self.model addObserver:self forKeyPath:@"status" Zeilenumbruch
options:NSKeyValueObservingOptionNew | Zeilenumbruch
NSKeyValueObservingOptionOld
context:NULL];
[self.model addObserver:self forKeyPath:@"objects" Zeilenumbruch
options:NSKeyValueObservingOptionNew context:NULL];
}

- (void)viewWillDisappear:(BOOL)inAnimated {
[self.model removeObserver:self forKeyPath:@"status"];
[self.model removeObserver:self forKeyPath:@"objects"];
[super viewWillDisappear:inAnimated];
}

Listing 2.63 Registrierung und Abmeldung eines weiteren Beobachters

Abbildung

Abbildung 2.31 Eine Outlet-Verbindung über den File’s Owner erstellen

Außerdem erweitern Sie die Methode observeValueForKeyPath:ofObject:change:context: folgendermaßen:

if([inKeyPath isEqualToString:@"status"]) {
NSLog(@"[+] Old status:%@", Zeilenumbruch
[inChange valueForKey:NSKeyValueChangeOldKey]);
NSLog(@"[+] New status:%@", Zeilenumbruch
[inChange valueForKey:NSKeyValueChangeNewKey]);
}
else {

[self.countLabel setText:[NSString stringWithFormat:@"%d", Zeilenumbruch
[inObject countOfObjects]]];
}

Listing 2.64 Anzeige der Anzahl im Label

Wenn Sie nun das Projekt ausführen und den Plusbutton des Steppers drücken, bleibt der Wert des Labels gleich, obwohl sich die Anzahl der Objekte im Array ändert. Das Key-Value-Observing bekommt diese Änderung allerdings nicht mit, weil es ja keinen entsprechenden Setter setCountOfObjects: im Model für diesen Wert gibt, den es beobachten kann.

Sie können jedoch Cocoa Touch vorgaukeln, dass Ihre App diesen Setter aufgerufen hat, indem Sie die Nachrichten willChangeValueForKey: und didChangeValueForKey:, jeweils vor beziehungsweise nach der Wertänderung an das Modell senden. Da nur die Methode updateDroids: des Modells das Array ändert, können Sie die Aufrufe für diese beiden Methoden dort unterbringen.

- (void)updateDroids:(NSInteger)inValue {
[self willChangeValueForKey:@"countOfObjects"];
if(inValue > [self.objects count]) {
...
[self.objects addObject:theDroid];
}
else if (inValue < [self.objects count]) {
[self.objects removeLastObject];
}
[self didChangeValueForKey:@"countOfObjects"];
}

Listing 2.65 Manuelles Versenden von Observierungsnachrichten

Nach dieser Änderung empfängt der Viewcontroller auch Observierungsnachrichten für den Schlüssel countOfObjects, und er passt den Wert nach einem Drücken des Plus- oder Minusbuttons entsprechend an. Die Verwendung dieser beiden Methoden hier ist allerdings ein Ausnahmefall, und wenn das Modell das Array an mehreren Stellen verändern würde, wäre dieses Vorgehen auch sehr unpraktisch.


Galileo Computing - Zum Seitenanfang

2.2.9 DelegationZur vorigen Überschrift

Sie sind im Beispiel oben bereits mit dem Thema Delegation in Kontakt gekommen, ohne dass die Beschreibung näher darauf eingegangen ist. Delegation ist ein zentrales Entwurfsmuster von Cocoa Touch, das sehr viele Klassen verwenden und das an vielen Stellen Vererbung ersetzt. Das klingt verwunderlich: Zunächst tippen wir uns die Finger wund, um Ihnen die Vorteile der Vererbung schmackhaft zu machen, und dann ist es auf einmal gut, sie zu vermeiden?

Die Lösung dieses scheinbaren Widerspruchs ist der Umstand, dass Sie das Mittel des Erbens nur einmal pro Klasse einsetzen können. Sie ist ein Joker, den Sie nur einmal ausspielen können. Wie die Astronauten sagen: »All zu viel ist ungesund« – und so ist auch die übermäßige Verwendung der Vererbung ein Designfehler.

Viele dieser Probleme lassen sich allerdings umgehen, wenn Sie die Möglichkeiten objektorientierter Sprachen ausnutzen, ohne dabei alles auf die Karte Vererbung zu setzen. Ein Entwurfsmuster, um die Funktionalitäten von Objekten anzupassen und zu erweitern, ist die Delegation. Ihr Prinzip ist dem vieler Vorgesetzter abgeschaut: Den täglichen Kleinkram, den sie selber nicht erledigen wollen oder können, geben sie einfach an andere weiter.

In der iOS-Programmierung funktioniert das ganz ähnlich. Wenn bei einer Klasse bestimmte Implementierungsdetails offen gelassen werden sollen, delegiert sie diese Aufgaben an ein anderes Objekt, das Delegate, das sich um diesen Kleinkram kümmert.

Für diesen Zweck besitzen viele Klassen eine Property delegate, die auf ein Delegate verweisen kann. Das Objekt, das auf das Delegate zeigt, nennt man dann das delegierende Objekt. Die Delegate-Methoden sind ereignisgesteuert. Das heißt, wenn das delegierende Objekt ein bestimmtes Ereignis durchläuft, ruft es die entsprechenden Methoden im Delegate auf – sofern es diese implementiert.

Die Beispiel-App verwendet bereits Delegation. Wenn Sie das Projekt in Xcode betrachten, sehen Sie, dass es eine Klasse AppDelegate besitzt:

Abbildung

Abbildung 2.32 Das »AppDelegate« der Beispiel-App

Das AppDelegate ist das Delegate eines Objekts der Klasse UIApplication, das die Applikation repräsentiert. Es ist das Application-Delegate der Beispiel-App. Sobald dieses Objekt ein Ereignis registriert, informiert es sein Delegate – im vorliegenden Beispiel also ein Objekt der Klasse AppDelegate – über dieses Ereignis. Zu diesen Ereignissen gehören beispielsweise alle, die den Lebenszyklus der Applikation betreffen.

Cocoa Touch verwendet für die meisten Delegate-Methoden ein einheitliches Muster. Der Methodenname beginnt in der Regel mit dem Namen der delegierenden Klasse, und mit dem ersten Parameter wird das delegierende Objekt übergeben. Typische Vertreter finden Sie in der Klasse AppDelegate der Beispiel-App:

- (BOOL)application:(UIApplication *)inApplication Zeilenumbruch
didFinishLaunchingWithOptions:(NSDictionary *)inOptions

Delegate-Methoden und ihre Namen

Viele Delegate-Methoden enthalten im Namen die Hilfsverben »should«, »will« oder »did«, die Auskunft über die Funktion der Methode geben. Über Methoden mit dem Namensbestandteil »should« fragt das delegierende Objekt den Delegate, ob es eine bestimmte Operation ausführen soll. Diese Methoden sollten dabei nur auf die gestellte Frage antworten und keine weitergehende Programmlogik enthalten. Die Namensbestandteile »will« oder »did« enthalten Methoden, deren Ausführung vor beziehungsweise nach einem bestimmten Ereignis erfolgt. Im Gegensatz zu den Methoden mit dem Bestandteil »should« sind diese Methoden dafür gedacht, Aktionen zu bestimmten Programmereignissen auszuführen.

Abbildung 2.33 stellt die Beziehung zwischen der delegierenden Klasse UITabBar und ihrem Delegate-Protokoll UITabBarDelegate grafisch dar. So löst beispielsweise der Aufruf der Methode setSelectedItem: in der Tabbar einen Aufruf der Delegate-Methode tabBar:didSelectItem: aus. Sie können die Delegation hier auch als Trennung der Zuständigkeiten sehen: Die Tabbar kümmert sich um die Darstellung und nimmt die Nutzereingaben entgegen, während das Delegate sich um die Auswertung kümmert.

Abbildung

Abbildung 2.33 Ein Delegate implementiert Methoden des Delegierenden.

Singletons

Die Klasse des Applikationsobjekts basiert auf einem weiteren Entwurfsmuster namens Singleton, das nur ein Objekt dieser Klasse im gesamten Programm zulässt. Sie können auf dieses Objekt jederzeit über die Klassenmethode sharedApplication zugreifen.

Die Klasse AppDelegate implementiert das Protokoll UIApplicationDelegate, was Sie an der entsprechenden Zeile in der Header-Datei des Delegates zu erkennen:

@interface HelloWorldAppDelegate : Zeilenumbruch
UIResponder<UIApplicationDelegate>

Dabei deklariert das Protokoll alle verfügbaren Methoden als optional. Nichtsdestotrotz sollte eine Applikation zumindest die wichtigsten Nachrichten abfangen, da sie ansonsten nicht auf wichtige Zustandsänderungen reagieren kann.

Eine der wichtigsten Delegate-Methoden des Application-Delegates ist application:didFinishLaunchingWithOptions: [Sie bemerken das Hilfsverb »did« im Methodennamen?]. Diese Methode ruft das Applikationsobjekt auf, nachdem das Betriebssystem die App gestartet hat. Die Projektvorlage hat diese Methode bereits implementiert:

- (BOOL)application:(UIApplication *)applicationZeilenumbruch
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:Zeilenumbruch
[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.viewController = [[ViewController alloc] Zeilenumbruch
initWithNibName:@"ViewController" bundle:nil];
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
return YES;

}

Listing 2.66 Startmethode des App-Delegates

Die Methode erzeugt ein Fenster und den Viewcontroller, verknüpft die beiden miteinander und bringt das Fenster über die Nachricht makeKeyAndVisible zur Anzeige. Dies ist ein greifbares Beispiel für das Prinzip der Delegation. Ohne eine Subklasse von UIApplication ableiten und ohne das Application-Singleton verändern zu müssen, können Sie auf die meisten Ereignisse der Applikation reagieren.

Abbildung 2.34 stellt alle Delegate-Methoden der Applikation zusammen mit den drei Applikationszuständen dar, wobei die gestrichelten Pfeile die Aufrufreihenfolge der Methoden zwischen den Zuständen angeben. Die durchgezogenen Pfeile zeigen die Zustandsübergänge an. Wenn die Applikation application:didFinishLaunchingWithOptions: aufruft, ist sie noch inaktiv. Beim Übergang in den aktiven Zustand sendet sie dann die Nachricht applicationBecomeActive:. Wenn Sie eine Applikation über den Home-Button des Geräts in den Hintergrund schicken, wechselt die Applikation in den Zustand Background.

Abbildung

Abbildung 2.34 Die Delegate-Methoden der Applikation

Sichern und Aufräumen

Sie sollten die Methoden applicationDidEnterBackground: und applicationWillTerminate: implementieren, um eventuell noch nicht gespeicherte Daten Ihrer App zu sichern. Außerdem können Sie diese Methoden für Aufräumarbeiten nutzen. Wenn Sie Ihr Programm in den Hintergrund schicken, sollten Sie immer so viel Speicher wie möglich freigeben. Auch während der Hintergrundausführung kann Ihr Programm noch Speicherwarnungen von Cocoa Touch empfangen.

Während es im Hintergrund ausgeführt wird, kann ein iOS-Programm nicht beliebig viel Rechenzeit in Anspruch nehmen. Sie dürfen in den Methoden applicationDidEnterBackground: und applicationWillTerminate: auch nicht einfach lange und aufwendige Operationen durchführen. Sie haben innerhalb dieser Methoden ungefähr fünf Sekunden Zeit, um notwendige Aufgaben auszuführen. Falls Ihre Implementierungen länger brauchen, wirft iOS Ihre App einfach aus dem Speicher.

In größeren Apps kann es sehr umständlich sein, alle Aufräumaktionen zentral vom Application-Delegate aus zu steuern – besonders wenn viele Viewcontroller entsprechende Operationen durchführen müssen. In diesen Fällen sollte jede Komponente selbst aufräumen. Viewcontroller sollten ihre Daten und verwendeten Ressourcen möglichst früh freigeben. Hierzu bieten sich beispielsweise die Methoden viewWillDisappear:, viewDidDisappear: und didReceiveMemoryWarning der Klasse UIViewController an.

Parallel zu den Delegate-Methoden sendet die Applikation indes auch entsprechende Benachrichtigungen (auch Notifications genannt). Das sind programminterne Nachrichten, die beliebige Objekte empfangen können. Die Namen der entsprechenden Benachrichtigungen finden Sie in der Apple-Dokumentation des Protokolls UIApplicationDelegate. Eine genauere Beschreibung von Notifications finden Sie in Kapitel 5, «Daten, Tabellen und Controller«.

Sie können das Verwenden von Delegate-Methoden im vorliegenden Beispiel selber testen. Fügen Sie in der Implementierungsdatei AppDelegate.m eine Implementierung für die Methode applicationDidReceiveMemoryWarning hinzu:

- (void)applicationDidReceiveMemoryWarning:Zeilenumbruch
(UIApplication *)inApplication {
NSLog(@"%@.%@ Einen Eimer für Monsieur!", self, Zeilenumbruch
NSStringFromSelector(_cmd));
}

Listing 2.67 Ich bin der finstere Sensenmann.

Diese Methode ruft das iOS bei knappem Hauptspeicher auf und ist eine Aufforderung, umgehend Ressourcen freizugeben. Wenn Sie dieser freundlichen Aufforderung nicht umgehend nachkommen, beendet das Betriebssystem gnadenlos Ihr Programm. Die Implementierung besteht in diesem Fall lediglich aus einer Konsolenmeldung. In der Praxis ist das natürlich nicht die angemessene Art, um auf knappen Speicher zu reagieren.

Wenn Sie die App übersetzen und starten und danach im Simulator den Menüpunkt HardwareSpeicherwarnhinweis simulieren aufrufen, der eine Speicherwarnmeldung erzeugt, können Sie folgende Ausgabe der Konsole beobachten:

Kapitel2[19738:f803] Received memory warning.
<AppDelegate:0x6a17cb0>.applicationDidReceiveMemoryWarning: Zeilenumbruch
Einen Eimer für Monsieur!

Listing 2.68 Das war wohl ein Pfefferminzblättchen zu viel.

Et voilà, die Applikation ruft die im Delegate implementierte Methode des UIApplicationDelegate-Protokolls korrekt auf, und das ganz ohne Vererbung.

Delegation vs. Kategorien

Wir haben im Abschnitt über Kategorien bereits vollmundig erklärt, dass Kategorien das Problem tiefer Vererbungshierarchien lösen. Das Gleiche haben wir Ihnen jetzt zum Thema Delegates auch erzählt. Inzwischen glauben Sie uns wahrscheinlich nichts mehr; wir bleiben jedoch hartnäckig. Delegates und Kategorien eignen sich zwar beide zur Vermeidung von Vererbung, sie sind jedoch nicht wesensverwandt oder gar austauschbar.

Kategorien verwenden Sie, um Eigenschaften von Klassen zu ändern. Damit gewinnen Sie keine Informationen über Ereignisse und Zustandsänderungen. Mit Delegates hingegen ändern Sie keine Eigenschaften. Delegates sind ein Mittel der Kommunikation und informieren über Zustandsänderungen und Ereignisse.

Im weiteren Verlauf des Buches werden Sie noch weitere Beispiele für die Delegation sehen. Die »passive« Verwendung, also die Implementierung eines Delegates mit den gewünschten Methoden, ist einfach – Sie haben ja gerade auch die Delegate-Methode applicationDidReceiveMemoryWarning: im Application-Delegate selber im-plementiert.

Der umgekehrte Weg, selbst eine Klasse zu erstellen, die Delegate-Methoden in einem Delegate aufrufen kann, ist etwas aufwendiger. Sie müssen dazu eine Klasse mit der gewünschten Funktionalität sowie ein Protokoll implementieren. Am einfachsten lässt sich das am Beispiel erklären.

Die Beispiel-App wird nun um eine Klasse Log erweitert, mit der sich Nachrichten loggen lassen. Das Logging erfolgt über eine Methode, die als Parameter die zu loggende Nachricht erhält. Über den Delegation-Mechanismus informiert die Klasse ihr Delegate, wenn der Log-Vorgang beendet ist. Das ist in der ersten Ausbaustufe natürlich keine umwerfende Funktionalität, jedoch genauso wie beim bisherigen Aufbau der Beispiel-App steht das Prinzip und nicht die Funktion im Vordergrund.

Der Viewcontroller verwendet die Klasse Log, um Nachrichten in die Konsole zu schreiben. Die Methode hierzu trägt den Namen logToConsole:. Sie ruft ihrerseits die Delegate-Methode logDidFinishLogging: auf.

Um diese Funktionalität abzubilden, muss das Log ein Delegate-Protokoll definieren, über eine Property verfügen, die den Verweis auf das Delegate hält, und die Methode logToConsole: implementieren. Da der Viewcontroller das Delegate ist, muss er das Protokoll implementieren. Außerdem erzeugt er das Log und sendet ihm die zu auszugebenden Meldungen.

Öffnen Sie die Beispiel-App, und fügen Sie eine neue Objective-C-Klasse hinzu. Geben Sie der Klasse den Namen »Log« und die Superklasse NSObject. Die Headerdatei Log.h enthält sowohl das besagte Protokoll als auch die Klassendeklaration selber. Die zyklische Abhängigkeit zwischen dem Protokoll und der Klasse löst hier eine Vorwärtsdeklaration des Protokolls auf.

#import <Foundation/Foundation.h>

@protocol LogDelegate;

@interface Log : NSObject

@property (nonatomic, weak) id<LogDelegate> delegate;

-(void)logToConsole:(NSString *)theMessage;

@end

@protocol LogDelegate<NSObject>

-(void)logDidFinishLogging:(Log *)inLog;

@end

Listing 2.69 Klasse und Delegate-Protokoll für das Loggen

Die Protokolldeklaration enthält den Namen und die Superklasse des Protokolls (analog zu Abschnitt 2.2.6) sowie die Methode, die eine Klasse implementieren muss, um dem Protokoll zu genügen. Da diese nicht mit dem Schlüsselwort @optional versehen ist, muss das Delegate diese Methode implementieren. Der Compiler weist Sie freundlicherweise darauf hin, wenn eine Klasse eine erforderliche Methode eines Protokolls nicht implementiert.

Die Log-Klasse enthält nur die Property delegate vom Typ id<LogDelegate>. Den Typ id kennen Sie ja bereits, und der angehängte Protokollname in spitzen Klammern gibt an, dass Sie dieser Property nur Objekte zuweisen dürfen, die das Protokoll LogDelegate implementieren.

In der Datei Log.m synthetisieren Sie die Property delegate und implementieren die Methode logToConsole:

#import "Log.h"

@implementation Log

@synthesize delegate;

-(void)logToConsole:(NSString *)theMessage {
NSLog(@"[+] %@.%@: %@", self, Zeilenumbruch
NSStringFromSelector(_cmd), theMessage);
[self.delegate logDidFinishLogging:self];
}
@end

Listing 2.70 Die Implementierung der Klasse Log

Die NSLog-Anweisung übernimmt neben den bereits bekannten Parametern noch die auszugebende Meldung. Im Anschluss ruft die Methode noch die Methode logDidFinishedLogging: des Delegates auf.

Damit der Viewcontroller die neue Klasse verwenden und das Protokoll implementieren kann, müssen Sie in dessen Headerdatei ViewController.h zunächst die Datei Log.h importieren und die Deklaration der Klasse ViewController um das Protokoll LogDelegate ergänzen:

#import "Log.h"

@interface ViewController : UIViewController<LogDelegate>

Listing 2.71 Das Interface des Viewcontrollers

Da der ViewController das besagte Protokoll unterstützen soll und die Methode logDidFinishLogging: erforderlich ist, fügen Sie in der Implementierung des Viewcontrollers diese Methode ein:

-(void)logDidFinishLogging:(Log *)inLog {
[self writeLog:@"Finished logging to console"];
}

Listing 2.72 Die Methode logDidFinishLogging: im Viewcontroller

Diese Methode macht also nichts weiter, als im Textview auf der GUI die Meldung auszugeben, dass das Log die Ausgabe beendet hat.

Um die Funktionalität zu testen, müssen Sie nun noch im Viewcontroller ein Log-Objekt erzeugen und diesem eine Nachricht senden. Dieses Objekt schreibt die Nachricht in die Konsole und ruft anschließend die Methode logDidFinishLogging: im Viewcontroller auf. Fügen Sie also ans Ende der Methode viewDidLoad des Viewcontrollers noch die folgenden Zeilen ein:

Log *theLog = [[Log alloc] init];
theLog.delegate = self;
[theLog logToConsole:[self.model name]];

Listing 2.73 Einbindung der Klasse »Log« im Viewcontroller

Die erste Zeile erzeugt ein Objekt der Klasse Log, und die zweite weist der Property delegate des neuen Objekts den Viewcontroller zu. Der Viewcontroller macht sich also zum Delegate des Logs. In der letzten Zeile sendet er die Methode logToConsole: an das Log-Objekt und übergibt ihr als Meldung den Namen des Modells. Testen Sie die Änderungen, und behalten Sie dabei sowohl die Konsole als auch die GUI der App im Auge.

Die Konsole zeigt pflichtgemäß die zu loggende Nachricht an:

2012-07-15 23:18:45.267 Kapitel2[78654:f803] [+] Zeilenumbruch
<Log: 0x6a5a100>.logToConsole:: LoremIpsum

Auf der GUI erscheint, um einige Millisekunden versetzt, die Nachricht »Finished logging to console« wie die folgende Abbildung zeigt.

Abbildung

Abbildung 2.35 Aufruf der Delegate-Methode im ViewController

Wie Sie anhand der Zeitstempel sehen, ist der Aufruf der Delegate-Methode zwei Millisekunden nach dem Schreiben der Nachricht in die Konsole erfolgt. Offensichtlich stimmen also Funktionalität und Timing – ein gutes Zeichen dafür, dass die Delegation funktioniert.

Typische Anwendungsfälle für Delegation

Das Beispiel oben ist bewusst einfach gehalten und dient nur zur Veranschaulichung des Prinzips der Delegation. Delegation ist insbesondere gut geeignet, um sich über Aktivitäten informieren zu lassen, die im Hintergrund asynchron ablaufen. Netzwerkzugriffe sind ein typisches Beispiel sowie die Verarbeitung von größeren Datenmengen. Sie müssen nicht die GUI einer App so lange blockieren, bis Sie ein großes Dokument eingelesen haben. Sie können beispielsweise das Laden im Hintergrund ablaufen lassen und sich per Delegation informieren lassen, wann dieser Auftrag beendet ist.



Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.







<< zurück
  Zum Katalog
Zum Katalog: Apps programmieren für iPhone und iPad

Apps programmieren für iPhone und iPad
Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Katalog: Einstieg in Objective-C 2.0 und Cocoa





 Einstieg in
 Objective-C 2.0
 und Cocoa


Zum Katalog: Apps entwickeln für iPhone und iPad - Videotraining






 Apps entwickeln für
 iPhone und iPad -
 Videotraining


Zum Katalog: Apps mit HTML5 und CSS3






 Apps mit HTML5
 und CSS3


Zum Katalog: iPhone- und iPad-Apps entwickeln






 iPhone- und
 iPad-Apps entwickeln


Zum Katalog: Android 4






 Android 4


Zum Katalog: Android-Apps entwickeln - Videotraining






 Android-Apps
 entwickeln -
 Videotraining


Zum Katalog: Windows Store Apps mit XAML und C#






 Windows Store Apps
 mit XAML und C#


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo





Copyright © Galileo Press 2013
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de