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 3 Views und Viewcontroller
Pfeil 3.1 Eigene View-Klassen in Cocoa Touch
Pfeil 3.1.1 Zeichnen in Cocoa Touch
Pfeil 3.1.2 Zeitberechnung
Pfeil 3.1.3 View-Erzeugung über NIB-Dateien
Pfeil 3.1.4 Aktualisierung der Zeitanzeige
Pfeil 3.1.5 Wiederverwendbarkeit von Views
Pfeil 3.2 Views und Viewcontroller
Pfeil 3.2.1 Outlets
Pfeil 3.2.2 Outlet-Collections
Pfeil 3.2.3 Containerviews
Pfeil 3.2.4 Viewhierarchien
Pfeil 3.2.5 Actions
Pfeil 3.2.6 Ereignisse
Pfeil 3.2.7 Controlzustände und Buttons
Pfeil 3.2.8 Direkte Gestenverarbeitung
Pfeil 3.2.9 Übergänge
Pfeil 3.2.10 Der Lebenszyklus eines Viewcontrollers
Pfeil 3.2.11 Speicher- und Ressourcenverwaltung des Viewcontrollers
Pfeil 3.3 Lokale Benachrichtigungen
Pfeil 3.3.1 Benachrichtigungen versenden
Pfeil 3.3.2 Benachrichtigungen verarbeiten
Pfeil 3.4 Eine App für alle
Pfeil 3.4.1 Das Retina-Display
Pfeil 3.4.2 Sprachkursus für die App
Pfeil 3.4.3 Es funktioniert nicht
Pfeil 3.4.4 Launch-Images und iPhone 5
Pfeil 3.4.5 Universelle Apps
Pfeil 3.5 Fehlersuche
Pfeil 3.5.1 Logging
Pfeil 3.5.2 Der Debugger
Pfeil 3.5.3 Die Debuggerkonsole
Pfeil 3.5.4 Laufzeitfehler

Galileo Computing - Zum Seitenanfang

3.2 Views und ViewcontrollerZur nächsten Überschrift

Das Beispielprojekt Clock zeigt zwar mehrere Uhren an, deren Zeiger bleiben aber stehen. Das liegt daran, dass dieses Programm nicht die Methode startAnimation aufruft. Wenn Sie diesen Aufruf in die Methode awakeFromNib in der Klasse ClockView einfügen, bewegen sich die Zeiger. Allerdings startet dann das Laden des Views die Timer, und erst der Aufruf der Methode dealloc der Uhren stoppt sie. Die Uhren laufen also ständig. Bei so einer kleinen App, die nur eine Ansicht zeigt, ist das zwar nicht schlimm. In größeren Projekten ist es sicherlich sinnvoller, die Timer nur dann laufen zu lassen, wenn die App sie auch tatsächlich anzeigt. Die Klasse UIView bietet dafür jedoch keine entsprechenden Methoden an, und dafür ist sie auch nicht da.

Für die generellen Abläufe in der Applikation ist schließlich die Controllerschicht zuständig. Und deshalb sollte der Viewcontroller diese Aufgabe übernehmen. In Cocoa Touch leiten sich die Viewcontroller von der Klasse UIViewController ab. Sie bietet vier Methoden an, die sie bei einer Änderung des Anzeigezustandes aufruft und die Sie in Ihren Unterklassen überschreiben können. Die Methoden heißen viewWillAppear:, viewDidAppear:, viewWillDisappear: und viewDidDisappear: und haben jeweils einen booleschen Parameter.

Während Cocoa Touch die ersten beiden Methoden bei der Anzeige des Views aufruft, ruft es die letzten beiden beim Verschwinden des Views auf. Der Namensbestandteil Will bedeutet, dass Cocoa Touch die Methode vor dem Ereignis aufruft, während Did darauf hinweist, dass Cocoa Touch die Methode nach dem Ereignis aufruft. Der boolesche Parameter gibt dabei jeweils an, ob die Anzeige oder das Verschwinden animiert erfolgt. Wenn Sie diese Methoden überschreiben, müssen Sie auch immer die entsprechende Methode in der Oberklasse aufrufen.

Sie könnten jetzt also die Uhren in viewDidAppear: starten und in viewWillDisappear: stoppen, und stellen damit sicher, dass die Uhr immer läuft, wenn sie die Applikation anzeigt. Sie müssen dazu nur noch Ihren Viewcontroller mit den Uhren bekannt machen.

Sie können in NIB-Dateien nicht nur den View-Aufbau speichern, sondern auch Verbindungen zwischen den Objekten im NIB und dem Eigentümer der NIB-Datei herstellen. (Der Eigentümer wird auch Files Owner genannt.)

Der Eigentümer ist dabei das Objekt, das die Datei geöffnet hat. Das ist in der Regel der Viewcontroller. Es gibt vier Verbindungstypen:

  • Actions sind Verbindungen zwischen Views, die Ereignisse versenden, und Methoden, die diese Ereignisse verarbeiten. Damit können Sie beispielsweise einen Methodenaufruf auslösen, wenn Sie einen Button drücken. Dieser Verbindungstyp ist Gegenstand des Abschnitts 3.2.5.
  • Ein Outlet verbindet ein Objekt in der NIB-Datei mit einem Attribut oder einer Property eines anderen Objekts. Auf der Seite des Eigentümers ist ein Outlet einfach ein Attribut oder besser eine Property, mit denen Sie dann auf das Objekt zugreifen können. Im zweiten Kapitel haben Sie zum Beispiel einen Textview über ein Outlet mit dem Viewcontroller verbunden.
  • Eine Outlet-Collection verbindet mehrere Objekte mit einem Array-Attribut [Cocoa Touch erlaubt zwar Outlets und Outlet-Collections über Ivars. Da Sie jedoch eine etwas eigentümliche Speicherverwaltung haben, sollten Sie lieber Propertys bevorzugen.] oder einer Array-Property. Sie können über Outlet-Collections also auf mehrere gleichartige Objekte des NIBs zugreifen. Diesen Verbindungstyp gibt es erst seit iOS 4.0.
  • Ein Segue (Übergang) verbindet in einem Storyboard unter iOS 5 jeweils einen Viewcontroller oder einen View, der Ereignisse versendet, mit einem Viewcontroller. Der Übergang beschreibt dabei den Wechsel von einem Viewcontroller zu einem anderen. Übergänge werden in Abschnitt 3.2.9 besprochen.

Galileo Computing - Zum Seitenanfang

3.2.1 OutletsZur nächsten ÜberschriftZur vorigen Überschrift

Outlets erzeugen Sie über den Interface Builder. Öffnen Sie dazu den View des Beispielprojekts, indem Sie die Datei AlarmClockViewController.xib auswählen. Aktivieren Sie in der Werkzeugleiste den Hilfseditor durch den mittleren Button in der Gruppe Editor (in Abbildung 3.17 oben rechts). Der Hilfseditor sollte nun die Headerdatei AlarmClockViewController.h des Viewcontrollers anzeigen. Sie können die angezeigte Datei über die Menüleiste des Editors unterhalb der Werkzeugleiste ändern, falls der Editor eine andere Datei geöffnet haben sollte.

Wenn Sie jetzt mit der rechten Maustaste (oder mit der linken bei gedrückter ctrl-Taste) auf das Ziffernblatt klicken, können Sie eine Verbindung von dem View zu der Headerdatei des Controllers ziehen, wie Abbildung 3.17 zeigt.

Abbildung

Abbildung 3.17 Ziehen einer Outlet-Verbindung

Nachdem Sie die Maustaste innerhalb der Klassendeklaration losgelassen haben, erscheint der in Abbildung 3.18 abgebildete Pop-over-Dialog, über den Sie ein Outlet oder eine Outlet-Collection anlegen können. Geben Sie clockView in das Textfeld ein, und lassen Sie die übrigen Einstellungen des Dialogs unverändert.

Durch einen Klick auf den Button Connect erstellen Sie im Quelltext ein Attribut und eine Property, vor deren Klasse die Markierung IBOutlet steht. Diese Markierung ist ein leeres C-Makro, das keine Auswirkung auf den erzeugten Programmcode hat. Es dient lediglich dazu, dem Interface Builder anzuzeigen, dass er zu dieser Property Verbindungen herstellen kann.

@property (nonatomic, retain) IBOutlet ClockView *clockView;

Abbildung

Abbildung 3.18 Pop-over-Dialog für das Outlet

Der Speicherverwaltungstyp der Property hängt davon ab, welches Speicherverwaltungsmodell Ihr Projekt verwendet. Bei manuellem Referenzenzählen hat die Property den Typ retain, und bei automatischem Referenzenzählen hat sie den Typ strong oder optional weak. Außerdem müssen Sie die Klasse ClockView noch im Header bekannt machen. Dazu können Sie entweder deren Header-Datei über die Anweisung #import "ClockView.h" importieren oder eine Vorwärtsdeklaration mit der Anweisung @class ClockView; vor der Deklaration der Controllerklasse einfügen. Eine Vorwärtsdeklaration sagt dem Compiler, dass es die entsprechende Klasse gibt und dass er das Auftreten des Klassennamens nicht als Fehler ansehen soll. Allerdings erfährt der Compiler durch eine Vorwärtsdeklaration nichts über die Details wie Methoden oder Propertys der Klasse – dazu ist eine Importanweisung notwendig. Vorwärtsdeklarationen sind jedoch sehr nützlich, um zyklische Imports zu vermeiden.

Hilfe bei der Speicherverwaltung

Xcode verändert nicht nur die Deklaration Ihrer Klasse, sondern auch deren Implementierung. Wenn Sie eine Outlet-Property anlegen, setzt Xcode in der Methode viewDidUnload die Property auf nil, und bei manuellem Referenzenzählen sendet es in dealloc außerdem noch ein release an das Attribut. Mit automatischem Referenzenzählen sollten Sie für Outlets den Speicherverwaltungstyp weak verwenden, sofern es sich bei dem verknüpften Objekt nicht um ein Element auf der obersten Ebene im NIB handelt (siehe dazu auch Abschnitt 3.2.11).

Anstelle von Outlet-Propertys können Sie auch Outlet-Attribute erzeugen. Dazu müssen Sie das Gummiband in den Attributblock, also zwischen die geschweiften Klammern, der Klassendeklaration ziehen. Allerdings halten sich Outlet-Attribute nicht an die Speicherverwaltungsregeln. Ein Outlet-Attribut bekommt ein retain gesendet, wenn es keinen Setter dafür gibt. Verwenden Sie also auch hier lieber Propertys als Attribute. Die neueren Xcode-Versionen legen allerdings keine Attributblöcke in den Header-Dateien mehr an, und Sie sollten darauf auch möglichst verzichten.

Abbildung

Abbildung 3.19 Der Verbindungsinspektor zeigt Verbindungen an.

Nach dem Laden der NIB-Datei verbindet Cocoa Touch diese Property mit dem View. Sie können sich die gesetzten und möglichen Verbindungen eines Objekts im Interface Builder über den Verbindungsinspektor ansehen (siehe Abbildung 3.19). Wenn Sie die Maus über eine Verbindung im Inspektor bewegen, hebt der Interface Builder den damit verbundenen View in der Ansicht hervor.

Indem Sie die entsprechenden Anweisungen in die Methoden viewDidAppear: und viewWillDisappear: einfügen, können Sie über den Viewcontroller jetzt eine Uhr starten. Die kompletten Methoden müssen Sie in die Implementierungsdatei AlarmClockViewController.m schreiben:

- (void)viewDidAppear:(BOOL)inAnimated {
[super viewDidAppear:inAnimated];
[self.clockView startAnimation];
}
- (void)viewWillDisappear:(BOOL)inAnimated {
[self.clockView stopAnimation];
[super viewWillDisappear:inAnimated];
}

Listing 3.18 Starten und Stoppen der Animation über den Viewcontroller

Außerdem sollten Sie die Headerdatei der Klasse ClockView in diese Implementierungsdatei einbinden, da Sie ansonsten Warnungen bei der Übersetzung erhalten. Dazu fügen Sie noch die Zeile

#import "ClockView.h"

an den Anfang der Implementierungsdatei ein. Hier reicht eine Vorwärtsdeklaration nicht aus, da der Controller ja die Methoden und Propertys der Klasse ClockView verwenden will. Setzen Sie diese Importanweisung unter die bereits enthaltene Importanweisung. Wenn Sie jetzt Ihr Programm im Simulator ausführen, bewegen sich die Zeiger einer Uhr.


Galileo Computing - Zum Seitenanfang

3.2.2 Outlet-CollectionsZur nächsten ÜberschriftZur vorigen Überschrift

Fügen Sie zum Testen noch weitere Uhranzeigen in Ihren View (z. B. so wie im Beispielprojekt Clock) ein, die Sie später allerdings wieder entfernen sollten. Im laufenden Programm bewegen sich dann nur die Zeiger der Uhr, die Sie bereits über ein Outlet mit dem Viewcontroller verbunden haben. Falls Sie eine andere Uhr aktivieren möchten, können Sie einfach ein Gummiband von einem anderen Clockview auf die Outlet-Property ziehen; dann bewegen sich jedoch nur die Zeiger dieser Uhr. Das liegt daran, dass der Viewcontroller immer nur die Uhr startet, auf die er über das Outlet zugreifen kann.

Sie können jetzt für jede Uhr auch ein eigenes Outlet mit den entsprechenden Start- und Stoppanweisungen anlegen, um die Uhren analog zu starten und zu stoppen. Das ist jedoch sehr unelegant und sieht nicht sehr schön aus, da Sie mehrmals die gleichen Anweisungen schreiben müssen. Für das mehrfache Ausführen der gleichen Anweisung gibt es doch schließlich Schleifen.

Seit iOS 4 können Sie Outlet-Collections verwenden, um mehrere Objekte auf einmal zu verbinden. Sie erzeugen sie ähnlich wie Outlets, indem Sie eine Verbindung von einem Objekt im Interface Builder in die Headerdatei des Controllers ziehen. Lassen Sie die Maustaste los, und wählen Sie im Pop-Over-Dialog unter Connection den Punkt Outlet Collection aus. In das Textfeld schreiben Sie clockViews. Nach einem Klick auf den Connect-Button legt Xcode eine Property mit dem Typ NSArray an und fügt eine Markierung IBOutletCollection(ClockView) vor der Deklaration des Typs ein. Die Deklaration in der Headerdatei sieht so aus:

@property (retain, nonatomic) IBOutletCollection(ClockView) Zeilenumbruch
NSArray *clockViews;

Außerdem hat Xcode die Implementierungsdatei verändert. Sie können jetzt der Outlet-Collection beliebig viele Clockviews zuweisen, indem Sie Gummibänder von den Views zu dieser Property-Deklaration ziehen. Der Makroparameter ClockView in der Deklaration schränkt im Interface Builder die möglichen Objekte für die Zuweisung auf die Klasse ClockView ein. Durch diesen Verbindungstyp können Sie jetzt zum Starten und Stoppen der Uhren eine Schleife verwenden:

- (void)viewDidAppear:(BOOL)inAnimated {
[super viewDidAppear:inAnimated];
for(ClockView *theView in self.clockViews) {
[theView startAnimation];
}
}
- (void)viewWillDisappear:(BOOL)inAnimated {
for(ClockView *theView in self.clockViews) {
[theView stopAnimation];
}
[super viewWillDisappear:inAnimated];
}

Listing 3.19 Starten und Stoppen der Uhren über eine Outlet-Collection

Der Verbindungsinspektor zeigt Ihnen die Outlet-Collections des Objekts in einer eigenen Rubrik an (siehe Abbildung 3.20), und Sie können einzelne Verbindungen durch einen Klick auf das x-Kreuzchen vor der Verbindung wieder lösen.

Abbildung

Abbildung 3.20 Outlet-Collections im Verbindungsinspektor

Achtung

Outlet-Collections verwenden zwar Arrays. Sie können jedoch die Reihenfolge der Elemente in dem Array nicht durch den Interface Builder festlegen.


Galileo Computing - Zum Seitenanfang

3.2.3 ContainerviewsZur nächsten ÜberschriftZur vorigen Überschrift

Wenn Sie mit Ihrer App eine ältere iOS-Version als 4 unterstützen möchten oder die Reihenfolge der Views wichtig ist, können Sie statt der Outlet-Collection auch einen View als Container verwenden. Sie legen dazu alle Views, auf die Sie gesammelt zugreifen möchten, in einem gemeinsamen, gegebenenfalls nicht sichtbaren View an, und auf diesen View greifen Sie über ein herkömmliches Outlet zu. Häufig verwendet man für diese Containerviews ein UIView mit transparenter Hintergrundfarbe, der nur zur Beherbergung anderer Views dient. An die einzelnen Views gelangen Sie dann über die Property subviews des Containerviews. Allerdings kann dabei jeder View nur in maximal einem Containerview liegen.

Unterstützung älterer iOS-Versionen

Programmierer neigen dazu, immer die aktuelle Version des Betriebssystems vorauszusetzen. Bei den Endanwendern sieht das allerdings anders aus. Apple unterstützt iOS 4 und erst recht iOS 5 nicht oder nur eingeschränkt auf älteren iPhones und iPod touches. Besitzer eines iPhone 2G müssen mit iOS 3 vorliebnehmen, und iOS 5 läuft nicht mehr auf einem 3G. Mit zunehmender Vielfalt von Cocoa-Touch-Geräten ist damit zu rechnen, dass es auch immer eine nicht gerade kleine Benutzergruppe gibt, die nicht die aktuelle Version von iOS verwenden kann.

Überlegen Sie daher vor der Verwendung neuer Features, welche Reichweite Ihre App haben soll. Falls die App bezüglich der Hardware-Ressourcen sehr anspruchsvoll ist oder wenn sie Funktionen benötigt, die erst ab iOS 5 oder iOS 6 zur Verfügung stehen, ist die Unterstützung älterer Geräte nicht möglich. Bei einer App ohne besondere Hardware-Anforderungen lässt sich der Kreis der potenziellen Benutzer dagegen allein dadurch vergrößern, dass die App auf allen möglichen Geräten läuft – auch wenn das bedeutet, dass Sie nicht alle Annehmlichkeiten der neueren iOS-Versionen verwenden können.

In unserem Beispielprojekt existiert bereits ein View, den Sie als Containerview verwenden können. Es ist der Hauptview, in den Sie alle Zifferblätter gelegt haben. Für diesen Containerview brauchen Sie auch kein gesondertes Outlet, da Sie über die Property view des Viewcontrollers bereits auf diesen View zugreifen können. Sie können also die Outlet-Collection im letzten Listing durch die Property subviews der view-Property des Viewcontrollers ersetzen. Sie müssen dazu nur die Zeilen mit den Schleifenköpfen durch

for(ClockView *theView in self.view.subviews) {

ersetzen. Das funktioniert allerdings nur, solange Sie keine Views einer anderen Klasse in den Hauptview legen.


Galileo Computing - Zum Seitenanfang

3.2.4 ViewhierarchienZur nächsten ÜberschriftZur vorigen Überschrift

Die Reihenfolge der Subviews im darüber liegenden View ist entscheidend für die Anzeige. Sie können sich die Reihenfolge in der Baumdarstellung des Views ansehen, zu der Sie gelangen, indem Sie auf das Symbol am unteren Fensterrand von Xcode 4 klicken (siehe Mauszeiger in Abbildung 3.21).

Abbildung

Abbildung 3.21 Umstellung auf Baumansicht (Mauszeiger)

In der Baumdarstellung können Sie sehen, wie die Views verschachtelt sind und in welcher Reihenfolge sie innerhalb einer Ebene liegen. Diese Anordnung wirkt sich auf die Darstellung und die Verarbeitung der Eingaben (oder auch Touches) aus. Je näher ein View zum Hauptview liegt, umso früher stellt Cocoa Touch ihn dar. Wenn also zwei Views einen gleichen Bereich auf dem Display verwenden, dann verdeckt der später dargestellte View den früher dargestellten.

In Abbildung 3.22 ist die Baumdarstellung eines Views zu sehen. Dabei haben die Views (1 bis 8) jeweils die gleichen Ziffern wie die Views in der Voransicht. Der rote View 2 ist am nächsten zum Hauptview, weswegen ihn alle anderen Views verdecken. Der gelbe View 6 liegt hinter dem grünen 3 und verdeckt somit diesen, und der schwarze View 7 liegt im gelben View, wodurch er diesen verdeckt.

Abbildung

Abbildung 3.22 Baumdarstellung eines Views

Änderung der Viewhierarchie

Sie können auch in der Baumdarstellung die Anordnung der Views verändern. Ziehen Sie dazu die Views einfach an die gewünschte Position. Wenn Sie dort einen View auf einen anderen ziehen, fügt der Interface Builder den gezogenen View als letzten Subview des anderen Views ein.

Die Objekte auf der obersten Ebene nennt man auch Top-Level-Objekte. Das ist in Abbildung 3.22 der View mit dem Namen Grau. Diese Objekte nehmen bezüglich der Speicherverwaltung – zumindest beim automatischen Referenzenzählen – eine Sonderstellung ein; dazu später mehr.


Galileo Computing - Zum Seitenanfang

3.2.5 ActionsZur nächsten ÜberschriftZur vorigen Überschrift

Bislang haben die verwendeten Views nur etwas auf dem Bildschirm dargestellt. Viele Views können indes auch Eingaben verarbeiten. Im Gegensatz zu den meisten Bildschirmen von Desktop-Computern ist bei iOS-Geräten das Ausgabegerät ja auch gleichzeitig das Eingabegerät. Diese Views haben in der Regel die Klasse UIControl als Oberklasse und werden im Folgenden auch kurz als Controls bezeichnet. Typische Controls sind beispielsweise Buttons, Schieberegler und Textfelder.

Neben den Outlets können Sie auch Action-Verbindungen herstellen. Mit diesen Verbindungen verknüpfen Sie die von den Controls gesendeten Ereignisse mit einer Methode eines Controllers. Wenn Sie eine Action-Verbindung zu einem Control erzeugen, bekommt das Control ein Zielobjekt und eine Actionmethode übergeben. Dieses Vorgehen bezeichnet man auch als Target-Action-Mechanismus.

Actionmethoden müssen eine von drei vorgegebenen Signaturen haben und zumindest in der Deklaration für den Rückgabetyp das Makro IBAction verwenden. Dieses Makro wird während der Übersetzung durch den C-Typ void ersetzt. Es zeigt dem Interface Builder an, dass es sich bei einer Methode um eine Actionmethode handelt, zu der Actionverbindungen erlaubt sind.

Die drei möglichen Signaturen für Actionmethoden sind:

-(IBAction)methodenName;
-(IBAction)methodenName:(id)inSender;
-(IBAction)methodenName:(id)inSender Zeilenumbruch
forEvent:(UIEvent *)inEvent;

Listing 3.20 Mögliche Signaturen für Actionmethoden

Den Methodennamen können Sie dabei natürlich frei wählen. Der erste Parameter in der zweiten und dritten Variante enthält das Control, das das Ereignis abgeschickt hat. Als Typ wurde hier id verwendet, der auf jede Klasse passt. Sie dürfen jedoch auch einen konkreten Typ, beispielsweise UIButton *, verwenden. Dann können Sie in der Methode auch die Punktnotation für dieses Control verwenden.

Über den Event-Parameter der dritten Variante können Sie zusätzliche Informationen über das auslösende Ereignis ermitteln. Darüber erhalten Sie beispielsweise die genauen Fingerpositionen zum Auslösezeitpunkt. Die liefert Ihnen der Event in Objekten der Klasse UITouch. Eine Möglichkeit, die Fingerposition zu bestimmen, stellt Listing 3.21 dar. Dabei bestimmt die Methode locationInView: die Fingerposition immer relativ zu dem angegebenen View. Das Listing berechnet sie also relativ zu dem Control, das das Ereignis ausgelöst hat.

- (IBAction)touchWithSender:(id)inSender
forEvent:(UIEvent *)inEvent {
UITouch *theTouch = [inEvent.allTouches anyObject];
CGPoint thePoint = [theTouch locationInView:inSender];
// Touchposition in thePoint auswerten
}

Listing 3.21 Ermittlung der Touchposition

Sie sollen jetzt einen Schalter (das ist ein View der Klasse UISwitch) dafür verwenden, den Animationsstatus einer Uhr aus dem Clock-Projekt zu steuern. Fügen Sie dazu einen UISwitch hinzu. Das ist ein Schiebeschalter mit zwei Zuständen für »an« und »aus«. Nach dem Einfügen ziehen Sie von dem Schiebeschalter ein Band in den Header des Viewcontrollers. In dem Pop-over-Dialog haben Sie nun eine zusätzliche Auswahlmöglichkeit Action. Füllen Sie diesen Dialog wie in Abbildung 3.23 gezeigt aus.

Abbildung

Abbildung 3.23 Anlegen einer Actionmethode

Das Control sendet das Ereignis Value Changed immer dann, wenn sich der Wert ändert, den das Control verwaltet. Bei einem UISwitch ist das ein boolescher Wert, der die Schalterstellung repräsentiert.

Sie können nun die Actionmethode wie folgt in der Datei AlarmClockViewController.m implementieren:

- (IBAction)switchAnimation:(UISwitch *)inSender {
if(inSender.on) {
[self.clockView startAnimation];
}
else {
[self.clockView stopAnimation];
}
}

Listing 3.22 Actionmethode zum Starten und Stoppen der Animation

Außerdem sollte die Methode viewWillDisappear: die Uhr stoppen. Wenn Sie nun das Projekt ausführen, können Sie die Uhr über den Schalter starten und stoppen. Allerdings steht nach dem Starten die Uhr, obwohl der Schalter auf »ein« steht. Sie können das umgehen, indem Sie entweder die Uhr in viewDidAppear: starten oder im Attributinspektor des Schalters dessen Anfangszustand auf »aus« ändern.

Eine weitere Alternative besteht darin, auch für den Schalter ein Outlet anzulegen. Die Methode viewDidAppear: kann dann den Wert des Schalters auslesen und in Abhängigkeit davon die Uhr starten. Der Vorteil dieser Lösung ist, dass Sie den Anfangszustand der Uhr über den Interface Builder, also ohne Veränderung des Programmcodes, einstellen können. Außerdem behält die Uhr ihren Animationszustand bei, wenn die App zwischenzeitlich einen anderen Viewcontroller anzeigt. Das ist bei diesem kleinen Projekt mit einem Viewcontroller vielleicht kein so großer Vorteil, kann allerdings bei sehr umfangreichen und komplexen Applikationen sehr angenehm sein. Wenn Sie die entsprechende Outlet-Property animationSwitch nennen, sieht der Code dafür so aus:

- (void)viewDidAppear:(BOOL)inAnimated {
[super viewDidAppear:inAnimated];
if(self.animationSwitch.on) {
[clockView startAnimation];
}
}

Listing 3.23 Starten der Animation in Abhängigkeit von einem Schalter

Alles für die Tonne

Denken Sie daran, die zusätzlichen Uhren wieder aus dem View zu entfernen. Sie brauchen sie für das Beispielprojekt nicht mehr. Ebenso brauchen Sie auch den Schalter nicht mehr. Sie können ihn also auch entfernen.


Galileo Computing - Zum Seitenanfang

3.2.6 EreignisseZur nächsten ÜberschriftZur vorigen Überschrift

Sie haben im Beispielprogramm das Ereignis Value Changed eines Schalters in einer Methode Ihres Viewcontrollers verarbeitet. Ein Control kann noch eine Reihe weiterer Ereignisse senden. Diese Ereignisse lassen sich in drei Kategorien unterteilen:

  • Ereignisse für Wertänderungen
  • Ereignisse für Gesten
  • Ereignisse für Textänderungen

Ereignisse für Wertänderungen

Dieses Ereignis haben Sie bereits kennengelernt, und es hat den Typ UIControlEventValueChanged. Es wird von Controls gesendet, die einen Wert manipulieren. Die Klassen, die dieses Ereignis versenden, finden Sie in Tabelle 3.1; die Spalte »Wert« enthält dabei den Namen der Methode, über die Sie den geänderten Wert abfragen können.

Tabelle 3.1 Controls, die das Ereignis »Value Changed« senden

Klasse Wert Beschreibung

UIDatePicker

date

Erlaubt die Auswahl eines Datums, einer Uhrzeit oder von beidem auf einmal. Außerdem kann dieser View einen animierten Countdown anzeigen.

UIPageControl

currentPage

Erlaubt die Navigation zwischen verschiedenen Seiten über kleine Punkte wie im Springboard.

UIRefreshControl

refreshing

Stellt Tableviews eine Standardfunktionalität zum Erneuern der Zellinhalte bereit.

UISegmentedControl

selectedSegmentIndex

Stellt mehrere Buttons nebeneinander dar, von denen der Nutzer aber nur einen auswählen kann (Radiobutton-Leiste).

UISlider

value

Erlaubt die nahezu stufenlose
Auswahl eines Wertes über
einen Schieberegler.

UISwitch

on

Ein Schalter mit zwei stabilen Zuständen

UIStepper

value

Erlaubt die Auswahl eines Wertes über einen Plus- und einen Minusknopf. Dieses Control gibt es seit iOS 5.

Ereignisse für Gesten

Ein wichtiger Erfolgsfaktor des iPhones ist dessen Gestensteuerung. Es gibt in iOS zwei Klassen von Gesten. Der Nutzer kann über Berührungsgesten Eingaben auf dem Bildschirm machen. Das können einfache Fingerdrücke oder komplexere Mehrfingerbewegungen auf dem Touchscreen sein. Dieser Abschnitt beschäftigt sich ausschließlich mit dieser Gestenklasse. Eine weitere grundsätzliche Möglichkeit, Eingaben vorzunehmen, sind Bewegungs- und Schüttelgesten, die wir in Kapitel 6, »Models, Layer, Animationen«, behandeln.

Berührungsgesten lösen eine Reihe von unterschiedlichen Ereignissen aus. Eine Berührungsgeste kann sich über mehrere Views erstrecken. Die Ereignisse werden hingegen immer an das Control gesendet, das das erste Ereignis einer Geste empfangen hat.

  • UIControlEventTouchDown leitet alle Gesten ein und sendet das Control bei der ersten Berührung.
  • UIControlEventTouchDownRepeat sendet das Control bei mehreren, schnell aufeinanderfolgenden Berührungen nach UIControlEventTouchDown. Sie können mit diesem Ereignistyp beispielsweise Doppel- oder Dreifachtaps auswerten. Die Anzahl der Taps können Sie über die Property tapCount aus den Touchobjekten des Ereignisses erfragen. Die Touchobjekte erhalten Sie über die Property allTouches der Klasse UIEvent (siehe Listing 3.21).
  • UIControlEventTouchUpInside beendet eine Geste, wenn sich der Finger zuletzt innerhalb des auslösenden Controls befunden hat. Sie verwenden diesen Ereignistyp in der Regel als Aktionsauslöser bei Buttons.
  • UIControlEventTouchUpOutside beendet eine Geste, wenn sich der Finger zuletzt außerhalb des auslösenden Controls befunden hat. Sie verwenden dieses Ereignis in der Regel, um den Abbruch einer Geste durch den Nutzer zu verarbeiten.
  • UIControlEventTouchCancel bricht eine Geste ab. Dieser Ereignistyp wird beispielsweise an Controls innerhalb eines Scrollviews gesendet, wenn Cocoa Touch erkennt, dass Sie mit der Geste scrollen möchten und die Ereignisse nicht das Control betreffen. Cocoa Touch sendet dieses Ereignis auch, wenn es die Gestenverarbeitung wegen einer Speicherwarnung abbrechen muss.
  • UIControlEventTouchDragInside zeigt eine Fingerbewegung innerhalb des Controls an.
  • UIControlEventTouchDragOutside zeigt eine Fingerbewegung außerhalb des Controls an.
  • UIControlEventTouchDragExit sendet das Control, wenn Sie die Finger aus dem Control heraus bewegen. Dieser Ereignistyp markiert den Übergang von UIControlEventTouchDragInside zu UIControlEventTouchDragOutside.
  • UIControlEventTouchDragEnter sendet das Control beim Verschieben des Fingers in das Control hinein. Dieser Ereignistyp markiert den Übergang von UIControlEventTouchDragOutside zu UIControlEventTouchDragInside.

Sie können sich sehr komplexe Gesten ausdenken und mit diesen Ereignistypen umsetzen. Allerdings scheinen die von außerhalb des Controls gesendeten Ereignisse nicht so zuverlässig zu funktionieren. Es kann beispielsweise vorkommen, dass Sie bei einer Fingerverschiebung aus dem Control heraus noch weitere UIControlEventTouchDragInside-Ereignisse empfangen. Die Umschaltung auf UIControlEventTouchDragOutside erfolgt teilweise erst, wenn sich der Finger schon lange außerhalb des Controls befindet. Wenn Sie also diese Ereignistypen verwenden möchten, sollten Sie ausgiebige Tests auf Geräten mit einplanen.

Viele Gesten lassen sich indes relativ einfach umsetzen, und Sie können beispielsweise Mehrfachtaps mit folgender Actionmethode auswerten, die Sie mit dem Ereignistyp UIControlEventTouchDownRepeat verbinden:

- (IBAction)handleMultiTap:(id)inSenderZeilenumbruch
forEvent:(UIEvent *)inEvent {
UITouch *theTouch = inEvent.allTouches.anyObject;
if(theTouch.tabCount == 2) {
// Doppel-Tap erkannt
}
}

Listing 3.24 Erkennung einer Double-Tap-Geste

In den Body der if-Abfrage können Sie den Code einsetzen, den Sie bei einem Doppeltap ausführen wollen. Die Zahl in der Bedingung gibt die notwendige Anzahl der Taps an. Wenn Sie sie entsprechend anpassen, können Sie mit dem Code auch Dreifach- oder Vierfachtaps erkennen.

Jede Geste sendet mindestens zwei Ereignisse: UIControlEventTouchDown und einen Touch-Up-Event. Sie können bei Gesten für Fingerbewegungen UIControlEventTouchDown zur Initialisierung der Geste und UIControlEventTouchUpInside oder -Outside zum Beenden verwenden. Gegebenenfalls sollten Sie auch das Ereignis UIControlEventTouchCancel auswerten. Über dieses Ereignis können Sie Änderungen bei einem Gestenabbruch rückgängig machen.

Die Gestenverarbeitung für Ereignisse lässt sich jedoch nur sehr schlecht wiederverwenden. Das ist gerade bei komplexen Gesten ungünstig. Für die Gestenverarbeitung gibt es noch andere Möglichkeiten (siehe Abschnitt 3.2.8).

Ereignisse für Textänderungen

Diese Ereignisse unterstützt derzeit nur die Klasse UITextField, die ein Feld für die Eingabe eines einzeiligen Textes darstellt. Sie können zwar auch hier Actionmethoden mit UIEvent-Parametern verwenden. Für diesen Parameter bekommen Sie allerdings immer nil übergeben.

  • UIControlEventEditingDidBegin markiert den Beginn der Eingabe in das Textfeld.
  • UIControlEventEditingDidChange zeigt die Änderungen des Textes im Textfeld an.
  • UIControlEventEditingDidEnd markiert das Ende der Eingabe in das Textfeld.
  • UIControlEventEditingDidEndOnExit markiert ebenfalls das Ende der Eingabe in das Textfeld. Das Textfeld sendet dieses Ereignis anscheinend nur, wenn der Nutzer das Textfeld durch Drücken des Return-Knopfes über die Systemfunktion beendet. Leider geht die Apple-Dokumentation auf dieses Ereignis nicht genauer ein.

Ein View, dessen Klasse keine Unterklasse von UIControl ist, kann keine Ereignisse verschicken. Wenn Sie indes ein Ereignis senden möchten, beispielsweise um die Berührung eines Bildes zu verarbeiten, können Sie das Bild einfach in einen View mit der Klasse UIControl legen. Sie legen ein solches Control wie einen Clockview an. Ziehen Sie ein Viewobjekt an die gewünschte Stelle, und ändern Sie dessen Klasse im Identitätsinspektor auf UIControl. Im Verbindungsinspektor dieses Views finden Sie dann alle beschriebenen Ereignisse. Sie können dagegen auch einen Button verwenden, der ebenfalls die Anzeige von Bildern unterstützt.


Galileo Computing - Zum Seitenanfang

3.2.7 Controlzustände und ButtonsZur nächsten ÜberschriftZur vorigen Überschrift

Neben den Ereignissen unterstützen Controls auch drei boolesche Systemzustände. Diese Zustände werden durch eine Bitmaske in der Property state des Controls abgebildet.

  • UIControlStateHighlighted ist aktiv, solange der Nutzer das Control gedrückt hält. Diesen Zustand können Sie auch über die Property highlighted abfragen oder setzen.
  • UIControlStateDisabled setzt das Control inaktiv. Es verarbeitet keine Eingaben und versendet auch keine Ereignisse. Diesen Zustand können Sie auch über die Property enabled abfragen oder setzen. Der Wert der Property ist dabei jedoch genau umgekehrt zu dem Wert in der Bitmaske.
  • UIControlStateSelected zeigt den Auswahlzustand des Controls an. Diesen Zustand können Sie auch über die Property selected abfragen oder setzen.

Ein Control kann mehrere Zustände gleichzeitig aktiviert haben. Wenn kein Zustand in der Bitmaske gesetzt ist, hat das Control den Zustand UIControlStateNormal. Am ausgiebigsten machen Buttons von den Zuständen Gebrauch.

Buttons sind relativ komplexe Controls, die mehrere Darstellungselemente unterstützen. Die Darstellungselemente können Sie in Abhängigkeit von den Zuständen setzen. Sie können für jede Zustandskombination eine eigene Darstellung festlegen. Außerdem können Sie über die Property adjustsImageWhenHighlighted zusätzlich festlegen, dass der Button beim Drücken noch einen Glüheffekt anzeigt. Abbildung 3.24 stellt rechts jeweils einen Button im Zustand highlighted ohne und mit Glüheffekt dar.

Abbildung

Abbildung 3.24 Die verschiedenen Darstellungstypen für Buttons

Ein Button kann einen von sechs vordefinierten Darstellungstypen haben, wobei fünf Typen ein festes Aussehen haben (siehe Abbildung 3.24, links) und Sie beim sechsten Typ, UIButtonTypeCustom, das Aussehen selbst bestimmen können.

Ein Custom-Button unterstützt drei Darstellungselemente: ein Vorder- und ein Hintergrundbild sowie einen Titel (siehe Abbildung 3.25). Für den Titel können Sie außerdem noch die Text- und die Schattenfarbe zustandsabhängig setzen.

Abbildung

Abbildung 3.25 Die Darstellungselemente eines Buttons

Sie können also über diese fünf Eigenschaften das wesentliche Aussehen des Buttons festlegen, wobei Sie das Vordergrundbild und den Titel über Insets beliebig positionieren können. Das können Sie entweder über die Propertys imageEdgeInsets beziehungsweise titleEdgeInsets oder über den Interface Builder (siehe Abbildung 3.26, unterer Bereich) machen. Über die Property contentEdgeInsets können Sie außerdem den kompletten Inhalt – also Bild und Titel – auf einmal positionieren.

Abbildung

Abbildung 3.26 Der Attributinspektor eines Buttons

Wenn Sie für eine Zustandskombination eine Darstellungseigenschaft nicht definieren, wird stattdessen die des Normalzustands verwendet. Im Interface Builder können Sie alle Eigenschaften für die einfachen Zustände, aber nicht für Kombinationen aus mehreren Zuständen festlegen. Sie können also beispielsweise das Hintergrundbild für den Zustand selected festlegen hingegen nicht für selected + highlighted. Das geht nur über Programmcode.

Durch die Möglichkeit, für die verschiedenen Buttonzustände unterschiedliche Darstellungen festzulegen, können Sie Ihren Apps relativ einfach optisch ansprechende Buttons hinzufügen und damit auch fehlende Buttontypen (beispielsweise Checkboxen) basteln; so lässt sich die Uhr auch über einen einrastenden Button anstatt eines Schalters steuern. Das Einrasten erreichen Sie durch die Invertierung des Zustands selected. Die Actionmethode switchAnimation: in der Datei AlarmClockViewController.m müssen Sie dazu wie folgt ändern:

- (IBAction)switchAnimation:(UIButton *)inSender {
inSender.selected = !inSender.selected;
if(inSender.selected) {
[self.clockView startAnimation];
}
else {
[self.clockView stopAnimation];
}
}

Listing 3.25 Actionmethode für einen einrastenden Button

Natürlich müssen Sie auch die Deklaration der Methode in der Headerdatei entsprechend ändern. Legen Sie im Interface Builder einen Button an, und stellen Sie über dessen Attributinspektor für die Zustände Default, Highlighted und Selected die Titel ein, klick beziehungsweise aus ein.

Vorsicht bei geänderten Methodensignaturen

Wenn Sie die Signatur einer Actionmethode im Programmcode ändern, sollten Sie alle Actionverbindungen im Interface Builder zu der ursprünglichen Methode löschen. Ansonsten erhalten Sie einen Laufzeitfehler. Besonders tückisch sind dabei die Änderungen zwischen Signaturen ohne und mit einem Parameter, da sie sich nur durch einen Doppelpunkt unterscheiden. Zwischen den Signaturen switchAnimation und switchAnimation: besteht ein Unterschied!

Verbinden Sie den Button mit der Actionmethode, indem Sie ein Band vom Button zum Files Owner ziehen. Wenn Sie das Band dort loslassen, erscheint ein Dialog, in dem Sie die Actionmethode auswählen können. Im Verbindungsinspektor des Buttons können Sie sehen, dass der Interface Builder automatisch die Actionmethode mit dem Ereignis Touch Up Inside verbunden hat. Der Button sendet dieses Ereignis, wenn der Finger den Button losgelassen hat.

Nach dem Start des Programms ist die Uhr ausgeschaltet und der Button zeigt die Beschriftung ein. Wenn Sie den Button drücken, wechselt die Beschriftung auf klick und nach dem Loslassen auf aus. In diesem Zustand verbleibt der Button, und die Uhr läuft.

Durch erneutes Drücken des Buttons wechselt die Beschriftung wieder auf ein. Der Button hat doch den Zustand highlighted; warum erscheint hier nicht auch die Beschriftung klick? Das liegt daran, dass in diesem Fall die Zustände highlighted und selected gesetzt sind. Für diese Zustandskombination haben Sie jedoch keinen Titel festgelegt, sodass der Button den Titel des Zustands default verwendet.

Den Titel für diese Zustandskombination können Sie, wie bereits erwähnt, nicht im Interface Builder festlegen, sondern müssen das im Programmcode machen. Dafür legen Sie ein Outlet auf den Button an. Die Klasse UIViewController besitzt eine weitere Methode, viewDidLoad, die Cocoa Touch aufruft, nachdem der View geladen wurde. In dieser Methode können Sie die Werte für diese Zustandskombination festlegen.

Da der Button für den Zustand selected + highlighted den gleichen Titel wie für den Zustand selected verwenden soll, kopiert die Methode den entsprechenden Titel aus dem Button. Dadurch können Sie den Titel allein über den Interface Builder pflegen. Durch diese Änderung zeigt der Button in der oben beschriebenen Situation jetzt auch den Titel klick anstatt ein an.

- (void)viewDidLoad {
[super viewDidLoad];
NSString *theTitle = [self.switchButton Zeilenumbruch
titleForState:UIControlStateHighlighted];
[self.switchButton setTitle:theTitle forState:Zeilenumbruch
UIControlStateSelected | UIControlStateHighlighted];
}

Listing 3.26 Initialisierung eines Buttons nach dem Laden des Views


Galileo Computing - Zum Seitenanfang

3.2.8 Direkte GestenverarbeitungZur nächsten ÜberschriftZur vorigen Überschrift

Wie bereits erwähnt, können Sie den Target-Action-Mechanismus für die Gestenverarbeitung einsetzen. Dieses Vorgehen ist indes recht aufwendig, wenn Sie eine Geste wiederverwenden möchten. Cocoa Touch bietet noch andere Möglichkeiten, um Gesten zu verarbeiten.

Damit ein View die Touch-Ereignisse empfängt, müssen Sie immer dessen Property userInteractionEnabled setzen. Cocoa Touch durchsucht in der View-Hierarchie nur diese Views und sendet die Ereignisse an den tiefsten View, unter dem der Berührungspunkt des Fingers liegt. Mit »der tiefste View« ist gemeint, dass es in diesem View keinen weiteren Subview an der Fingerposition mit gesetzter Property userInteractionEnabled gibt.

Der View muss auch wollen

Sie können über userInteractionEnabled die Touchverarbeitung komplett ausschalten. Wenn Sie diesen Property-Wert also auf NO setzen, reagieren weder der View noch dessen Subviews mehr auf Berührungen. Dabei ist es egal, ob Sie ein Control verwenden oder die Touches direkt oder über eine andere Methodik auswerten. Bei userInteractionEnabled = NO ignoriert der View Ihre Berührungen – mögen Sie ihn auch noch so zärtlich streicheln.

Die Klasse UIView stellt zwei Methoden bereit, um den Subview zu ermitteln, der die Gesten verarbeitet. Die Methode hitTest:withEvent: bestimmt zu einem Punkt den Subview, der die Gestenverarbeitung übernehmen soll. Sie verwendet dazu die Methode pointInside:withEvent:, die entscheidet, ob der angegebene Punkt in dem View liegt oder nicht. Während der Eventparameter bei beiden Methoden nil sein darf, müssen Sie den Punkt relativ zu dem Koordinatensystem des Views angeben. Wenn Sie diese Methode aus dem Programmcode aufrufen, müssen Sie also in der Regel den Punkt erst umrechnen.

Die Klasse UIView stellt dafür die Methoden convertPoint:fromView: und convertPoint:toView: bereit. Die erste Methode rechnet den Punkt aus dem Koordinatensystem des zweiten Parameters in das Koordinatensystem des Nachrichtenempfängers um. Bei der zweiten Methode sind die Koordinatensysteme des zweiten Parameters und des Empfängers bei der Berechnung vertauscht.

... und raus bist du!

Wenn Sie die View-Bestimmung für die Gestenverarbeitung anpassen möchten, sollten Sie in Ihren Views die Methode pointInside:withEvent: überschreiben. Ein Überschreiben der Methode hitTest:withEvent: ist in der Regel nicht notwendig.

Sofern Sie den Property-Wert clipsToBounds eines Views auf NO setzen, kann jeder Subview des Views über dessen Grenzen hinaus zeichnen. Allerdings empfängt der Subview nur da Touches, wo er auch innerhalb seiner umgebenden Views liegt. Diese Situation stellt Abbildung 3.27 dar: Cocoa Touch stellt den Subview, der in der Hierarchie im Superview liegt, zwar komplett auf dem Bildschirm dar; dieser empfängt jedoch nur Ereignisse für die Gesten in dem gekennzeichneten, berührungsempfindlichen Bereich. Die anderen Berührungen erhält in diesem Beispiel der Hauptview.

Abbildung

Abbildung 3.27 Berührungsempfindlicher Bereich eines Subviews

Gestenverarbeitung über Responder

Die Klasse UIResponder ist die direkte Oberklasse von UIView und UIViewController. Sie stellt mehrere Methoden zur Auswertung von Gesten bereit. Die Methoden für die Berührungsgesten haben jeweils zwei Parameter und liefern void zurück. Der erste Parameter ist eine Menge der Klasse NSSet von UITouch-Objekten, die eine Fingerberührung auf dem Bildschirm repräsentieren. Der zweite Parameter ist ein UIEvent-Objekt.

Der Methodenaufruf von touchesBegan:withEvent: leitet eine Gestensequenz ein. Die folgenden Fingerbewegungen erhält der Responder über die Methode touchesMoved:withEvent:. Abgeschlossen wird die Geste über einen Aufruf von entweder touchesEnded:withEvent: oder touchesCancelled:withEvent:. Dabei kennzeichnet ein Aufruf der Methode touchesCancelled:withEvent:, dass das System eine Geste abbrechen möchte (siehe dazu die Beschreibung zu UIControlEventTouchCancel in Abschnitt 3.2.6).

Diese Methoden sind die Grundlagen der Gestenverarbeitung. Allerdings führt die Verwendung dieser Methoden in der Regel zu einer relativ festen Koppelung von View und Controller, da der View meistens das Ergebnis einer Geste an den Controller weiterreichen muss. Falls Sie dennoch eine eigene Gestenverarbeitung über diese Responder-Methoden realisieren möchten, sollten Sie die Kommunikation mit dem Controller über Delegation realisieren. Eine direkte Verwendung von Controller-Eigenschaften im View ist eine Verletzung des MVC-Architekturmusters, weil dadurch eine Abhängigkeit des Views von der Controllerschicht entsteht.

Responder-Methoden und Delegation

In Kapitel 2, »Grundlagen«, haben Sie bereits die Delegation als Muster kennengelernt. Um den View mit der Controllerschicht lose zu koppeln, erstellen Sie ein Protokoll, das die Delegate-Methoden deklariert. Außerdem spendieren Sie dem View eine Property, um ihn mit dem Delegate verknüpfen zu können. Dann können die Responder-Methoden des Views immer eine Delegate-Methode aufrufen, wenn sie die gewünschte Geste erkannt haben. Sie sollten also nicht die Events einfach weiterleiten, sondern lieber die erkannten Gesten.

Angenommen, Ihr View soll Kreisbewegungen erkennen, dann könnte das Protokoll die Delegate-Methode circleView:didDetectsCircleWithCenter:radius: bereitstellen, die der View nach der Erkennung eines Kreises aufruft. Wenn Sie diese Geste aber in unterschiedlichen Views erkennen möchten, sollten Sie dafür lieber einen Gesture Recognizer einsetzen.

Gestenverarbeitung über Controls

Eine andere Möglichkeit, einen Controller lose an einen View zu koppeln, ist die Verwendung des Target-Action-Mechanismus über eigene Unterklassen von UIControl. Diese Klasse stellt ebenfalls vier Methoden zur Gestenverarbeitung bereit, die den Methoden der Klasse UIResponder sehr ähneln. Ihre Namen lauten beginTrackingWithTouch:withEvent:, continueTrackingWithTouch:withEvent: und endTrackingWithTouch:withEvent:, und sie erhalten jeweils zwei Parameter, wovon der erste ein Touchobjekt und der zweite der Event ist. Im Gegensatz zu den Responder-Methoden erhalten Sie hier also nicht alle Berührungen des Bildschirms als direkte Parameter. Falls Sie eine Mehrfingergeste implementieren möchten, können Sie jedoch alle Berührungen über die Methode allTouches des Events abfragen. Außerdem gibt es noch die Methode cancelTrackingWithEvent:, die das Control analog zu touchesCancelled:withEvent: beim Abbruch einer Geste aufruft.

Über die Gestenverarbeitung soll aus der Uhr ein Wecker entstehen. Bei einem analogen Wecker gibt es neben den Zeigern für die Zeitanzeige einen weiteren Zeiger für die Alarmzeit. In der Wecker-App soll der Nutzer diesen Zeiger durch Berührung und Bewegung mit dem Finger einstellen können. Das Control muss also eine Drehbewegung mit einem Finger auf dem Bildschirm erkennen. Diese Bewegung ähnelt dem Wählen auf einem Telefon mit Wählscheibe. Das Control soll dabei auch die Darstellung des Alarmzeigers übernehmen. Legen Sie zunächst eine neue Klasse ClockControl als Unterklasse von UIControl an.

Das Control erhält für die Darstellung der Alarmzeit die Property time mit dem Typ NSTimeInterval, der eine Zeitdauer in Sekunden als Fließkommawert enthält. Sie können die Alarmzeit über die Methode angle in einen Winkel umrechnen und entsprechend über die Methode setAngle: als Winkelwert setzen. Dabei sind 12 Stunden genau

12 × 60 × 60 = 43200 Sekunden

und die entsprechen einem Winkel von 360°, und damit können Sie aus der Zeit t über

2tp / 43200 = tp / 21600 = w

den Winkel w berechnen. Wenn Sie die letzte Gleichung mit 21600 / p multiplizieren, erhalten Sie die Gleichung

t = 21600w / p

um die Zeit aus dem Winkel zu berechnen. Diese beiden Gleichungen bilden die Grundlage für die Implementierung der Methoden angle und setAngle:

- (CGFloat)angle {
return self.time * M_PI / 21600.0;
}

- (void)setAngle:(CGFloat)inAngle {
self.time = 21600.0 * inAngle / M_PI;
}

Listing 3.27 Umrechnung der Alarmzeit in einen Winkel und umgekehrt

Da auch das Control die Methoden midPoint und pointWithRadius:angle: benötigt, lagern Sie sie aus der Klasse ClockView in die Kategorie UIView(AlarmClock) aus. Rufen Sie dazu den Menüpunkt FileNewFile... auf, und wählen Sie die Vorlage Objective-C class extension in der Rubrik iOSCocoa Touch aus. Wenn Sie im folgenden Dialog »AlarmClock« unter Extension Name und »UIView« unter Class eingeben (siehe Abbildung 3.28), legt Xcode beim Speichern die Dateien UIView+AlarmClock.h und UIView+AlarmClock.m an. In die Headerdatei tragen Sie die Deklarationen für die beiden Methoden ein. Außerdem sollten Sie die Systemheaderdatei für das UIKit importieren. Die komplette Datei finden Sie in Listing 3.28.

Abbildung

Abbildung 3.28 Anlegen einer Kategorie für die Wecker-App

#import <UIKit/UIKit.h>

@interface UIView (AlarmClock)

- (CGPoint)midPoint;
- (CGPoint)pointWithRadius:(CGFloat)inRadiusZeilenumbruch
angle:(CGFloat)inAngle;

@end

Listing 3.28 Deklaration der Kategorie

Analog verschieben Sie die Implementierungen dieser beiden Methoden aus der Klasse ClockView in den Implementierungsblock der Kategorie in der Datei UIView+AlarmClock.m. Durch das Entfernen der Methoden zeigt Xcode in der Klassenimplementierung Fehler an. Um sie zu eliminieren, sollten Sie die Kategorie in die Datei ClockView.m importieren. Dazu fügen Sie die Anweisung #import "UI-View+AlarmClock.h" vor dem Implementierungsblock ein. Damit Sie die Methoden auch im neuen Control verwenden können, fügen Sie die Importanweisung außerdem an den Anfang von ClockControl.m ein.

Die Methode angleWithPoint: berechnet über die C-Funktion atan2 zu einem Punkt im Control den Winkel der Linie, die vom Mittelpunkt zu diesem Punkt führt:

- (CGFloat)angleWithPoint:(CGPoint)inPoint {
CGPoint theCenter = [self midPoint];
CGFloat theX = inPoint.x – theCenter.x;
CGFloat theY = inPoint.y – theCenter.y;
CGFloat theAngle = atan2f(theX, -theY);
return theAngle < 0 ? theAngle + 2.0 * M_PI : theAngle;
}

Listing 3.29 Berechnung des Winkels zu einem Punkt

Der Winkel, den die Funktion atan2 berechnet, liegt zwischen –p und p. Negative Werte müssen Sie also auf entsprechende positive Werte umrechnen. Abbildung 3.29 stellt diese beiden Fälle grafisch dar. Dabei befinden sich positive Werte auf der rechten und negative auf der linken Kreishälfte.

Abbildung

Abbildung 3.29 Umrechnung von Punkten in Winkel

Der Zeiger soll sich jedoch nur verstellen lassen, wenn die Fingerberührung in der Nähe des Zeigers stattfindet. Das können Sie am einfachsten erreichen, wenn Sie die Methode pointInside:withEvent: überschreiben:

- (BOOL)pointInside:(CGPoint)inPoint
withEvent:(UIEvent *)inEvent {
CGFloat theAngle = [self angleWithPoint:inPoint];
CGFloat theDelta = fabs(theAngle – self.angle);
return theDelta < 2 * M_PI / 180.0;
}

Listing 3.30 Prüfung, ob ein Punkt in der Nähe des Alarmzeigers liegt

Die Variable theDelta enthält den Winkelabstand des Fingers vom Zeiger. Wenn dieser Winkel kleiner als 2° ist, soll das Control den Touch verarbeiten. Mit diesen Methoden können Sie nun die Gestenverabeitung implementieren. Der Inhalt der drei Methoden beginTrackingWithTouch:withEvent:, continueTrackingWithTouch:withEvent: und endTrackingWithTouch:withEvent: ist dabei fast gleich, sodass das Control für diesen Code sinnvollerweise die Hilfsmethode updateAngleWithTouch: bereitstellt. Sie ermitteln jeweils aus der Fingerposition den Punkt im Control und rechnen diesen über die Methode angleWithPoint: in den entsprechenden Winkel um. Diesen Winkel übergeben Sie an die Methode setAngle:, die daraus die entsprechende Weckzeit berechnet. Nach der Neuberechnung müssen die Methoden noch den Zeiger für die Weckzeit durch einen Aufruf von setNeedsDisplay neu zeichnen lassen. Die letzte Anweisung der Methode versendet schließlich das Value-Changed-Ereignis. Sie ruft also bei den entsprechenden Targets des Controls die jeweilige Actionmethode auf.

- (void)updateAngleWithTouch:(UITouch *)inTouch {
CGPoint thePoint = [inTouch locationInView:self];
self.angle = [self angleWithPoint:thePoint];
[self setNeedsDisplay];
[self sendActionsForControlEvents:Zeilenumbruch
UIControlEventValueChanged];
}

Listing 3.31 Aktualisierung der Daten in den Tracking-Methoden

Die Methode cancelTrackingWithEvent: sollte den Ausgangszustand des Controls bei Gestenabbruch wiederherstellen. Dazu muss es den aktuellen Winkel zu Beginn der Gestenverarbeitung speichern und in dieser Methode wieder an setAngle: übergeben. Für die Speicherung verwendet das Control eine private Property savedAngle. Die Implementierung dieser Methoden sieht also wie folgt aus:

- (BOOL)beginTrackingWithTouch:(UITouch *)inTouchZeilenumbruch
withEvent:(UIEvent *)inEvent {
self.savedAngle = self.angle;
[self updateAngleWithTouch:inTouch];
return YES;
}

- (BOOL)continueTrackingWithTouch:(UITouch *)inTouchZeilenumbruch
withEvent:(UIEvent *)inEvent {
[self updateAngleWithTouch:inTouch];
return YES;
}

- (void)endTrackingWithTouch:(UITouch *)inTouchZeilenumbruch
withEvent:(UIEvent *)inEvent {
[self updateAngleWithTouch:inTouch];
}

- (void)cancelTrackingWithEvent:(UIEvent *)inEvent {
self.angle = self.savedAngle;
[self setNeedsDisplay];
}

Listing 3.32 Die Methoden für die Gestenverarbeitung

Jetzt fehlt dem Control nur noch die Implementierung der Methode drawRect:, die einfach nur einen blauen Strich als Zeiger zeichnet. Dabei verwendet sie während der Gestenverarbeitung einen transparenten Blauton als Rückmeldung für den Nutzer. Diesen Zustand können Sie über die Property tracking der Klasse UIControl erfragen.

- (void)drawRect:(CGRect)inRectangle {
CGContextRef theContext = UIGraphicsGetCurrentContext();
CGRect theBounds = self.bounds;
CGPoint theCenter = [self midPoint];
CGFloat theRadius = CGRectGetWidth(theBounds) / 2.0;
CGPoint thePoint = [self pointWithRadius:theRadius * 0.7 Zeilenumbruch
angle:self.time * M_PI / 21600.0];

CGContextSaveGState(theContext);
CGContextSetRGBStrokeColor(theContext,Zeilenumbruch
0.0, 0.0, 1.0, self.tracking ? 0.5 : 1.0);
CGContextSetLineWidth(theContext, 7.0);
CGContextSetLineCap(theContext, kCGLineCapRound);
CGContextMoveToPoint(theContext, Zeilenumbruch
theCenter.x, theCenter.y);
CGContextAddLineToPoint(theContext, Zeilenumbruch
thePoint.x, thePoint.y);
CGContextStrokePath(theContext);
CGContextRestoreGState(theContext);
}

Listing 3.33 Zeichnen des Alarmzeigers

In der XIB-Datei können Sie jetzt das Control in den Clockview legen, wenn Sie dem Control eine transparente Hintergrundfarbe geben. Ziehen Sie dazu zuerst ein UIView-Objekt aus der Bibliothek auf das Ziffernblatt, und weisen Sie ihm die Klasse ClockControl über dessen Identitätsinspektor zu. Das Control sollte die gleiche Größe wie der Clockview haben. Außerdem müssen Sie dessen Property userInteractionEnabled im Attributinspektor auf YES setzen, damit Cocoa Touch die Gesten auch an das Control sendet.

Um Ihr neues Control zu testen, legen Sie ein Label in den View und verbinden es über ein Outlet mit dem Namen timeLabel mit dem Viewcontroller. In diesem Label soll der Wecker die Alarmzeit als Text anzeigen. Außerdem verbinden Sie das Control im Interface Builder über den Ereignistyp Value Changed mit der Methode updateTimeLabel des Viewcontrollers, deren Implementierung Sie in Listing 3.34 finden. Sobald Sie den Alarmzeiger bewegen, sendet das Control ja dieses Ereignis. Die Methode schreibt die aktuelle Weckzeit in das Label. Die Variable theTime enthält dabei die Alarmzeit in Minuten. Die Stunden und Minuten für die Anzeige berechnen Sie daraus durch eine Division durch 60, wobei der Divisionsrest der Minutenanteil ist.

- (IBAction)updateTimeLabel {
NSInteger theTime = round(self.clockControl.time / 60.0);
NSInteger theMinutes = theTime % 60;
NSInteger theHours = theTime / 60;

self.timeLabel.text = [NSString stringWithFormat:Zeilenumbruch
@"%d:%02d", theHours, theMinutes];
}

Listing 3.34 Erzeugung einer Zeichenkette aus einer Alarmzeit

Wenn Sie den Alarmzeiger in der App bewegen, können Sie sehen, dass der Controller auch die angezeigte Alarmzeit kontinuierlich aktualisiert. Dem Wecker fehlt jetzt nur noch die Weckfunktion, also das Klingeln. Außerdem muss der Viewcontroller beim Loslassen des Zeigers den Alarm konfigurieren. Diese beiden Themen behandelt Abschnitt 3.3, und die Implementierung der Weckfunktion geschieht über die Actionmethode updateAlarm, die Sie über das Ereignis Touch Up Inside mit dem Control verbinden.

Mehrere Ereignistypen behandeln

Der Viewcontroller verarbeitet also zwei unterschiedliche Ereignistypen des Controls. Das ist gerade bei Controls, die kontinuierliche Werte liefern, sinnvoll. Der Ereignistyp Value Changed dient zur Aktualisierung des Views und somit zur interaktiven Rückmeldung des Zustands an den Nutzer, während das Ereignis Touch Up Inside die Verarbeitung des endgültigen Wertes in der App auslöst. Sie können jedoch auch noch andere Ereignisarten sinnvoll verwenden. Beispielsweise können Sie durch den Ereignistyp Touch Up Outside einen Abbruch der Operation ermöglichen, indem Sie dafür eine Actionmethode implementieren, die den alten Zustand wiederherstellt.

Gestenverarbeitung über Gesture Recognizer

Mit der iOS-Version 3.2 [Diese Version war auf das iPad beschränkt. Für alle anderen Gerätetypen gibt es die Gesture Recognizer erst ab iOS 4.0.] beziehungsweise 4.0 hat Apple eine weitere Möglichkeit der Gestenverarbeitung eingeführt. Die Klasse UIGestureRecognizer ermöglicht eine Implementierung von Gestensteuerungen, die von den Views getrennt ist. Damit können Sie jede beliebige Gestenverarbeitung zu jedem View hinzufügen. Der Gesture Recognizer trennt also die Eingabeverarbeitung von der Darstellung des Views. Dadurch sind diese beiden Aufgaben (Darstellung und Ereignisverarbeitung) vollkommen unabhängig voneinander oder auch orthogonal zueinander, und Sie können die entsprechenden Komponenten (Views und Gesture Recognizer) nahezu beliebig miteinander kombinieren.

Die Klasse UIView bietet zwei Methoden und eine Property zum Verwalten von Gesture Recognizern an. Mit addGestureRecognizer: fügen Sie einem View einen neuen Gesture Recognizer hinzu, den Sie mit removeGestureRecognizer: wieder entfernen können. Mit der Property gestureRecognizers können Sie alle Gesture Recognizers des Views abfragen oder auf einmal setzen. Mit Xcode 4.2 können Sie die Gesture Recognizer auch direkt über die Bibliothek (siehe Abbildung 3.30) des Interface Builders zu Ihren Views hinzufügen, indem Sie sie auf die Views ziehen.

Abbildung

Abbildung 3.30 Die Gesture Recognizer in Xcode 4.2

Die Klassen sind in der gleichen Reihenfolge wie die Icons in Abbildung 3.30 aufgeführt.

  1. UITapGestureRecognizer erkennt einzelne oder mehrere Berührungen des haltenden Views. Sie können sowohl die notwendige Tap-Zahl als auch die Anzahl der beteiligten Finger zur Auslösung vorgeben.
  2. UIPinchGestureRecognizer erkennt die Bewegung von zwei Fingern aufeinander zu oder voneinander weg. Diese Geste wird meistens zum Heraus- beziehungsweise Hereinzoomen verwendet.
  3. UIRotationGestureRecognizer erkennt die Bewegung von zwei sich umeinander drehenden Fingern, wie sie viele Apps für die Drehung von Bildern eingesetzen.
  4. UISwipeGestureRecognizer erkennt Wischbewegungen. Sie können die Anzahl der beteiligten Finger angeben, jedoch damit nur die Richtung und keine Distanzen auswerten.
  5. UIPanGestureRecognizer erkennt Verschiebe-Bewegungen in einem View. Sie können damit beispielsweise das Verschieben eines Views innerhalb eines anderen durchführen (Dragging), da Sie mit diesem Recognizer auch die Richtung und die Distanz der Geste ermitteln können. Auch bei diesem Gesture Recognizer können Sie die Anzahl der beteiligten Finger vorgeben.
  6. UILongPressGestureRecognizer erkennt längere Berührungen des zugrunde liegenden Views. Diese Gestenauswertung eignet sich sehr gut, wenn ein View unterschiedliche Gestenarten unterstützen soll. Beispielsweise können Sie eine Karte scrollen und durch längeres Drücken eine Markierung setzen. Auch bei diesem Gesture Recognizer können Sie die Anzahl der beteiligten Finger einschränken. Außerdem können Sie eine minimale Dauer für die Berührungslänge wählen.

Sie können auch eigene Unterklassen der Klasse UIGestureRecognizer schreiben und so eine eigene Gestenverarbeitung implementieren. Dafür besitzt die Klasse die gleichen vier Methoden, die auch die Klasse UIResponder bereitstellt. Zusätzlich gibt es eine Methode reset. Sie können diese Methode überschreiben, um nach dem Abschluss einer Geste Ihren Gesture Recognizer für die nächste Geste zurückzusetzen.

Gesture Recognizer unterstützen wie Controls den Target-Action-Mechanismus – allerdings ohne Ereignistypen. Sie können über die Methode addTarget:action: eine neue Aktion hinzufügen, die Sie mit removeTarget:action: auch wieder entfernen können.

Das Beispielprojekt verwendet einen UILongPressGestureRecognizer, um die Alarmzeit zusätzlich zur Zeigerdrehung einstellen zu können. Eine Berührung des Ziffernblatts, die länger als vier Zehntelsekunden dauert, schaltet den Alarmzeiger ein und setzt ihn an die Position des Fingers. Legen Sie einen Longpress Recognizer an, indem Sie das entsprechende Objekt aus der Bibliothek auf das Clockview-Objekt in der Baumansicht ziehen.

Das Ein- und Ausschalten des Alarmzeigers und des Labels erfolgt über die Property alarmHidden, die auch die Sichtbarkeit dieser Views steuert. Sie ändert außerdem noch den Zustand eines Schalters über das Outlet alarmSwitch, mit dem der Nutzer den Alarm explizit ein- oder ausschalten kann.

- (BOOL)alarmHidden {
return self.clockControl.hidden;
}

- (void)setAlarmHidden:(BOOL)inAlarmHidden {
self.alarmSwitch.on = !inAlarmHidden;
self.clockControl.hidden = inAlarmHidden;
self.timeLabel.hidden = inAlarmHidden;
}

Listing 3.35 Sichtbarkeit der Alarmanzeige steuern

Die Zuordnung ist wichtig

Der Gesture Recognizer funktioniert nicht richtig, wenn Sie ihn nicht zu dem View, sondern zu dem Control hinzufügen. Sie sollen ihn ja auch bei ausgeschaltetem Alarm benutzen können. Da Sie in diesem Zustand allerdings das Control versteckt haben, beachtet Cocoa Touch dann auch den Recognizer nicht. Das Ziffernblatt ist hingegen immer sichtbar. Sie können die Zuordnung des Recognizers übrigens in dessen Verbindungsinspektor überprüfen.

Die Actionmethode eines Gesture Recognizers hat entweder keinen oder genau einen Parameter. In diesem Parameter bekommen Sie den Gesture Recognizer übergeben, und er sollte dessen Typ haben. Die Actionmethode für den Wecker muss die Position des Fingers auslesen. Dazu bietet die Klasse UIGestureRecognizer die Methode locationInView:, mit der Sie die Fingerposition relativ zu dem Koordinatensystem eines Views berechnen können. Die Actionmethode updateClockHand: können Sie jetzt folgendermaßen implementieren:

- (IBAction)updateAlarmHand:Zeilenumbruch
(UIGestureRecognizer *)inRecognizer {
CGPoint thePoint =Zeilenumbruch
[inRecognizer locationInView:self.clockControl];
CGFloat theAngle =Zeilenumbruch
[self.clockControl angleWithPoint:thePoint];
self.clockControl.angle = theAngle;
[self.clockControl setNeedsDisplay];
self.alarmHidden = NO;
[self updateTimeLabel];
if(inRecognizer.state == UIGestureRecognizerStateEnded) {
[self updateAlarm];
}
}

Listing 3.36 Actionmethode zur Aktualisierung des Alarmzeigers

Die Methode updateAlarm am Ende von Listing 3.36 sorgt dafür, dass der Wecker zur eingestellten Zeit auch klingelt. Wie das genau funktioniert, erfahren Sie, wie bereits erwähnt, in Abschnitt 3.3, »Lokale Benachrichtigungen«. Wenn Sie den Gesture Recognizer durch eine Berührung auslösen, ruft er die Actionmethode so lange auf, bis Sie den Finger wieder vom Bildschirm nehmen. Er registriert also auch alle Fingerbewegungen. Die App braucht indes die Auslösung des Alarmtons nicht jedes Mal zu aktualisieren. Eine Aktualisierung reicht aus, wenn der Nutzer den Zeiger loslässt. Diese Situation können Sie über die Property state des Gesture Recognizers feststellen. Wenn sie den Wert UIGestureRecognizerStateEnded hat, hat der Nutzer die Geste beendet.

Die Qual der Wahl

Sie haben drei Möglichkeiten zur Gestenverarbeitung kennengelernt. Welche ist denn nun die beste? Das hängt natürlich von dem Anwendungsfall ab. Tabelle 3.2 listet die verschiedenen Möglichkeiten für die Gestenverarbeitung auf.

Tabelle 3.2 Vor- und Nachteile verschiedener Möglichkeiten zur Gestenerkennung

Gestenverarbeitung Vor- und Nachteile

Überschreiben der
Responder-Methoden

Vorteile

Die Gestenverarbeitung auch im Viewcontroller und nicht nur im View möglich.

Auswertung mehrerer unterschiedlicher Gesten innerhalb eines Views

Nachteile

Gefahr der Verletzung des MVC-Musters: Abhängigkeit des Views vom Controller

Die Gestenverarbeitung ist mit der Viewklasse fest verbunden, was die Wiederverwendung sehr stark einschränkt.

Unterklasse von UIControl

Vorteile

Nutzung des Target-Action-Mechanismus und dadurch keine Abhängigkeit des Views vom Viewcontroller

Nutzung der Action-Connections im Interface Builder

Hohe Wiederverwendbarkeit möglich

Nachteile

Keine eigenen Eventtypen

Unterklasse von UIGestureRecognizer

Vorteile

Gestenverarbeitung komplett von der Darstellung getrennt

Viele fertige Recognizer vorhanden

Höchster Wiederverwendungsgrad, da die Gestenerkennung komplett von der Darstellung getrennt ist

Der Hauptnachteil bei der Verwendung der Responder-Methoden (die enge Kopplung des Views an den Viewcontroller) kann natürlich auch bei undurchdachter Im-plementierung der anderen Verfahren auftreten. Bei der Verwendung des Responder-Mechanismus tappen jedoch viele Programmierer in diese Falle. Sie können eine starke Koppelung jedoch – wie beschrieben – über Delegation vermeiden.


Galileo Computing - Zum Seitenanfang

3.2.9 ÜbergängeZur nächsten ÜberschriftZur vorigen Überschrift

Icon

Mit iOS 5 und den Storyboards hat Apple auch Segues (Übergänge) eingeführt. Diese Objekte beschreiben einen Übergang von einem Viewcontroller zu einem anderen. Übergänge haben somit als Verbindungstyp im Interface Builder einen sehr eingeschränkten Anwendungsbereich. Andererseits können Sie darüber schon im Interface Builder eine Verknüpfung zwischen den UI-Elementen und den Views festlegen. Deswegen nennt Apple diese neuen Beschreibungsdateien für die Views auch Storyboards.

Um die Funktionsweise der Übergänge zu veranschaulichen, erstellen Sie ein kleines Projekt. Legen Sie dazu in Xcode ein neues iPad-Projekt des Typs Single View Application an, und geben Sie an, dass dieses Projekt ein Storyboard und ARC verwenden soll. Öffnen Sie das Storyboard, und legen Sie einen neuen Button in den dort angezeigten View. Danach ziehen Sie einen neuen Viewcontroller (siehe Abbildung 3.31) auf die Zeichenfläche des Storyboards.

Abbildung

Abbildung 3.31 Die Viewcontroller im Interface Builder

Als Nächstes legen Sie einen Übergang an, indem Sie bei gedrückter rechter Maustaste ein Gummiband vom Button zu dem neuen Viewcontroller ziehen. Nachdem Sie in dem schwarzen Dialog den Eintrag performSegueWithIdentifier:sender: ausgewählt haben, zeigt der Interface Builder zwischen den Viewcontrollern einen grauen Pfeil mit einem Symbol in der Mitte an. Im Attributinspektor dieses Übergangs können Sie jetzt festlegen, wie der Wechsel erfolgen soll. Wählen Sie unter Style die Option Modal und unter Presentation den Punkt Form Sheet aus (siehe Abbildung 3.32).

Abbildung

Abbildung 3.32 Ein Übergang zwischen zwei Viewcontrollern

Starten Sie das Projekt im Simulator, und klicken Sie den Button. Der Simulator graut die weiße Fläche aus und schiebt von unten ein weißes Rechteck auf den Bildschirm. Dieses Rechteck ist der neue Viewcontroller, den Sie im Storyboard angelegt haben.

Mit dieser Konfiguration zeigt die App ihn als modalen Dialog an. In der Methode prepareForSegue:sender: können Sie den neu erzeugten Viewcontroller konfigurieren. Der Übergang ruft diese Methode bei dem Ursprungsviewcontroller auf, bevor er den Zielviewcontroller anzeigt. Wenn Sie mehrere Übergänge von einem Viewcontroller aus aufrufen können, können Sie diese anhand ihrer Kennung unterscheiden. Die Kennung können Sie über das Eingabefeld Identifier im Attributinspektor des Übergangs setzen (siehe Abbildung 3.32). Die Unterscheidung im Programmcode kann dann anhand der Property identifier erfolgen, die die Kennung des Übergangs enthält. Listing 3.37 gibt Ihnen ein Beispiel dafür.

#import "SegueViewController.h"

@implementation SegueViewController

- (void)prepareForSegue:(UIStoryboardSegue *)inSegueZeilenumbruch
sender:(id)inSender {
if([inSegue.identifier isEqualToString:@"dialog"]) {
id theDialog = inSegue.destinationViewController;
// theDialog initialisieren
}
}
@end

Listing 3.37 Initialisierung eines Controllers bei einem Übergang

Sie können Übergänge auch im Code über die Methode performSegueWithIdentifier:sender: auslösen. Mit dieser Methode lassen sich also Übergänge auch über Actionmethoden aufrufen:

- (IBAction)performSegue:(id)inSender {
[self performSegueWithIdentifier:@"dialog"
sender:inSender];
}

Listing 3.38 Auslösung eines Übergangs


Galileo Computing - Zum Seitenanfang

3.2.10 Der Lebenszyklus eines ViewcontrollersZur nächsten ÜberschriftZur vorigen Überschrift

Der Viewcontroller verwaltet den View; er lädt ihn aus einer NIB-Datei oder einem Storyboard. Wie erzeugt Cocoa Touch hingegen den Viewcontroller? Auf diese Frage gibt es zwei Antworten. Entweder liegt er als serialisiertes Objekt in einer XIB-Datei beziehungsweise einem Storyboard, oder das Programm erzeugt ihn wie andere Objekte auch über eine direkte Anweisung im Code.

Für den initialen Viewcontroller der Wecker-App kommt, wenn Sie Xcode 4.2 oder neuer verwenden, die letzte Variante zum Einsatz. Wenn Sie die Implementierung der Klasse AlarmClockAppDelegate öffnen, finden Sie in der Methode application:didFinishLaunchingWithOptions: den entsprechenden Code. Die zweite Anweisung in der Methode – das ist die Zuweisung zu der Property viewController – enthält die Erzeugung des Viewcontrollers.

- (BOOL)application:(UIApplication *)inApplicationZeilenumbruch
didFinishLaunchingWithOptions:(NSDictionary *)inOptions {
self.window = [[[UIWindow alloc] initWithFrame:Zeilenumbruch
[[UIScreen mainScreen] bounds]] autorelease];
self.viewController = Zeilenumbruch
[[[AlarmClockViewController alloc]Zeilenumbruch
initWithNibName:@"AlarmClockViewController"Zeilenumbruch
bundle:nil] autorelease];
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
return YES;
}

Listing 3.39 Erzeugung des Viewcontrollers in der Wecker-App

Ältere Xcode-Versionen haben dagegen eine weitere XIB-Datei mit dem Namen MainWindow.xib angelegt. Über diese Datei legt Xcode das Application-Delegate, den Viewcontroller und das Fenster der App an. Sie finden diese Objekte jeweils als Top-Level-Objekte in der XIB-Datei.

Sie können Ihre Viewcontroller und auch Objekte von beliebigen Klassen in NIB-Dateien anlegen. Dazu ziehen Sie das entsprechende Controllerobjekt (siehe Abbildung 3.31) einfach in die linke Seitenleiste des Interface Builders und verbinden es über ein Outlet mit dem Files Owner. Wenn Sie Objekte mit anderen Klassen anlegen wollen, verwenden Sie stattdessen den orangenen Würfel (siehe Abbildung 3.33). Sie können so Objekte mit nahezu beliebigen Klassen in der XIB-Datei oder im Storyboard anlegen.

Abbildung

Abbildung 3.33 Bibliothekssymbol für beliebige Objekte

Wenn Sie diese Objekte nach der Erzeugung initialisieren möchten, sollte deren Klasse eine Initialisierungsmethode init oder initWithCoder: besitzen, wobei Cocoa Touch Letztere bevorzugt. Alternativ können Sie auch den Initialisierungscode in die Methode awakeFromNib schreiben (siehe Abbildung 3.15). Zu beliebigen Objekten in NIB-Dateien folgen später noch Beispiele.

Tipp

Wenn Sie einen einfachen Viewcontroller über eine NIB-Datei erzeugen, sollten Sie unbedingt auf zwei Dinge achten. Erstens müssen Sie im Identitätsinspektor die Klasse des Viewcontrollers setzen. Andernfalls erzeugt Ihre App für den Viewcontroller ein Objekt mit der Klasse UIViewController. Zweitens sollten Sie im Attributinspektor des Controllers die richtige XIB-Datei mit dem passenden View auswählen.

Auch bei beliebigen Objekten müssen Sie deren Klasse im Identitätsinspektor setzen, ansonsten legt Ihnen Cocoa Touch ein schnödes NSObject an. Übrigens können Sie in diesen Objekten auch Outlets und Actionmethoden anlegen, die Sie mit Ihren Views verknüpfen können.

Sie haben bereits einige Methoden kennengelernt, die Cocoa Touch zu bestimmten Zeitpunkten im Leben eines Viewcontrollers aufruft. Wenn Sie beispielsweise die Methode viewDidAppear: überschreiben, können Sie eigenen Programmcode ausführen, unmittelbar nachdem Ihre App den View des Viewcontrollers angezeigt hat. Cocoa Touch benachrichtigt Sie also über ein bestimmtes Ereignis, das Ihren Viewcontroller betrifft. In Abbildung 3.34 ist der komplette Lebenszyklus eines Viewcontrollers anhand seiner Initialisierungs- und Benachrichtigungsmethoden sowie der Property view dargestellt. Bis auf die Property view dürfen Sie allerdings keine dieser Methoden direkt aufrufen. Sie dürfen sie nur überschreiben.

Abbildung

Abbildung 3.34 Der Lebenszyklus eines Viewcontrollers

Richtig überschreiben

Sie dürfen alle Methoden aus Abbildung 3.34 überschreiben. Dabei sollten Sie jedoch in der Methode – bis auf view, setView: und loadView [ [Bei ARC gehört hier natürlich auch dealloc dazu.] – auch immer die Methode in der Oberklasse aufrufen. Bei den drei genannten Methoden können Sie die Methode der Oberklasse aufrufen, müssen es jedoch nicht tun.

Bei initialisierenden Methoden wie awakeFromNib oder viewDidAppear: sollte der Aufruf immer am Anfang der Methode erfolgen. Bei den finalisierenden Methoden wie viewWillDisappear: oder viewDidUnload erfolgt der Aufruf der Supermethode hingegen erst am Schluss der überschreibenden Methode. Für die Methoden viewDidAppear: und viewWillDisappear: sieht das beispielsweise so aus:

- (void)viewDidAppear:(BOOL)inAnimated {
[super viewDidAppear:inAnimated];
// Hier steht Ihr Code
}
- (void)viewWillDisappear:(BOOL)inAnimated {
// Hier steht Ihr Code
[super viewWillDisappear:inAnimated];
}

Seit iOS 6 hat Apple die Methoden viewWillUnload und viewDidUnload als veraltet markiert. Sie dürfen sie zwar weiterhin verwenden, allerdings kann Apple die Unterstützung dafür mit einer zukünftigen iOS-Version ganz einstellen. Abschnitt 3.2.11, »Speicher- und Ressourcenverwaltung des Viewcontrollers«, geht noch genauer darauf ein, wie Sie die Verwendung dieser Methoden umgehen können.

Die Methoden initWithCoder: und awakeFromNib kennen Sie ja bereits von den View-Klassen. Diese Methoden kommen zum Zuge, wenn Sie den Viewcontroller in einer NIB-Datei oder einem Storyboard erzeugen.

Sie können einen Viewcontroller auch aus Ihrem Programmcode erzeugen. Dazu dienen die Methoden init und initWithNibName:bundle:. Wenn Sie init verwenden, müssen Sie entweder die Methode loadView implementieren, oder die NIB-Datei des Viewcontrollers muss bis auf die Dateiendung genauso wie die Klasse Ihres Viewcontrollers heißen. Wenn sie allerdings einen anderslautenden Namen haben soll, sollten Sie stattdessen die Initialisierungsmethode initWithNibName:bundle: verwenden, wobei Sie für den Bundle-Parameter nil verwenden können.

Das Überschreiben der Methode awakeFromNib ist in vielen Fällen sinnlos, da der Viewcontroller zum Zeitpunkt ihres Aufrufs den View noch nicht geladen hat und somit die Outlets noch nicht initialisiert sind. Es sind nur die Outlets gesetzt, die in die NIB-Datei verweisen, die den Viewcontroller erzeugt. Ein Beispiel dazu gibt Abbildung 3.35, die die Hierarchien von zwei fiktiven Viewcontrollern enthält. Dabei lädt das Objekt View Controller auf der linken Seite seinen View aus dem NIB auf der rechten Seite. Der Eigentümer (Files Owner) der rechten NIB-Datei ist ein Platzhalter für das Viewcontroller-Objekt aus dem linken NIB. Der Viewcontroller verlinkt also über Outlets sowohl auf Objekte aus dem linken als auch aus dem rechten NIB.

Wenn Cocoa Touch nun die Methode awakeFromNib des Viewcontrollers aufruft, bestehen nur die Outlets zu den beiden Objekten Navigation Controller und Object aus dem linken NIB, da er das rechte NIB ja noch nicht geladen hat. Die Verbindung zum View und die Outlets zum Label und dem Imageview hat Cocoa Touch hingegen noch nicht hergestellt. Diese Verbindungen kommen erst durch den Aufruf der loadView-Methode zustande.

Abbildung

Abbildung 3.35 Outlets zwischen XIB-Dateien

Regeln für Outlets

Auf das Objekt eines Outlets eines Viewcontrollers können Sie

frühestens in viewDidLoad zugreifen, wenn Sie das Outlet vom Files Owner zum Objekt gezogen haben.schon in awakeFromNib zugreifen, wenn die Verbindung von dem Viewcontroller in der Rubrik Objects ausgeht.

Der Viewcontroller lädt den View beim ersten Zugriff auf die Property view. Das geschieht vor der ersten Anzeige des Views. Sie können allerdings auch das Laden erzwingen, indem Sie die Property auslesen. Sie können dafür Ihrem Viewcontroller beispielsweise die folgende Methode hinzufügen:

- (BOOL)forceLoadView {
return self.view != nil;
}

Listing 3.40 Methode zum Laden des Views eines Viewcontrollers

Der Zugriff auf die Property in der zweiten Zeile erzwingt das Laden des Views. Den Ladezustand des Views können Sie über die Methode isViewLoaded erfragen.

Die Methode loadView lädt den View aus der NIB-Datei. Sie können diese überschreiben, um den View durch Code zu erzeugen. Dazu weisen Sie der Property view einfach Ihren erstellten View zu. In diesem Fall sollten Sie indes nicht die Methode in der Oberklasse aufrufen. Cocoa Touch ruft nach dem Laden des Views die Methode viewDidLoad auf, die Sie ja auch bereits kennengelernt haben.

Tipp

Wenn Sie Ihren Viewcontroller oder dessen View nach dem Laden initialisieren möchten, sollten Sie dazu die Methode viewDidLoad überschreiben. Wegen der fehlenden Outlet-Verbindungen ist ja die Methode awakeFromNib in den meisten Fällen nicht dafür geeignet. Die Methode loadView ist zum Laden des Views gedacht und sollte auch nur dafür überschrieben werden.

Sie können jetzt Ihren Viewcontroller beliebig oft anzeigen und wieder verschwinden lassen. In diesen Anzeigezyklen ruft Cocoa Touch viewWillAppear:, viewDidAppear:, viewWillDisappear: und viewDidDisappear: auf, die Sie ja bereits kennengelernt haben. Das nächste Kapitel stellt mehrere Möglichkeiten vor, Viewcontroller anzuzeigen und wieder verschwinden zu lassen.


Galileo Computing - Zum Seitenanfang

3.2.11 Speicher- und Ressourcenverwaltung des ViewcontrollersZur vorigen Überschrift

Ein Viewcontroller verwaltet in der Regel mehrere Ressourcen. Das sind beispielsweise Views und andere Objekte, die er für seine Aufgabe benötigt und die seinen Zustand abbilden. Dabei besteht ein Unterschied zwischen wiederherstellbaren und nicht wiederherstellbaren Ressourcen. Zum Beispiel können Sie Views in der Regel wiederherstellen, indem Sie sie aus einem NIB oder Storyboard laden beziehungsweise die Programmanweisungen mit deren Erzeugungscode wieder ausführen. Nicht wiederherstellbare Daten können Nutzereingaben sein, die Sie nicht anderweitig gespeichert haben.

Der Controller kann und sollte wiederherstellbare Ressourcen freigeben, sobald er sie nicht mehr benötigt. Dadurch vermindern Sie den Speicherverbrauch und erhöhen die Stabilität der App. Der Freigabezeitpunkt hängt dabei in der Regel vom Erzeugungszeitpunkt ab, und die Lebenszeit einer Ressource lässt sich für gewöhnlich einem der drei Zyklen des Viewcontrollers zuordnen:

  1. Ressourcen, die der Controller in den Initialisierermethoden oder in awakeFromNib erzeugt, sollte er auch erst bei seiner Zerstörung wieder freigeben. Diese Ressourcen haben also den gleichen Lebenszyklus wie der Vewcontroller. Dazu gehören beispielsweise alle Outlets, die in dem NIB liegen, das das Viewcontroller-Objekt enthält. Das sind in Abbildung 3.35 alle Outlets auf der linken Seite.
  2. Erstellt der Controller die Ressource beim Laden des Views, so sollten Sie sie auch erst mit dem View freigegeben. Zu den Ressourcen des Ladezyklus gehören beispielsweise der View und die wiederherstellbaren Ressourcen, die Sie in der Methode viewDidLoad erzeugen. Die Freigabe erfolgt entweder automatisch oder in einer der Methoden viewWillUnload, viewDidUnload oder didReceiveMemoryWarning. Alle Outlets auf der rechten Seite von Abbildung 3.35 gehören hierzu.
  3. Sie können Ressourcen auch innerhalb des Viewzyklus verwalten; also beispielsweise in viewWillAppear: anfordern und in viewWillDisappear: wieder freigeben.

Diese drei Fälle sind Faustregeln, von denen es aber auch durchaus Ausnahmen geben kann. Wie dem auch sei – Sie sollten die Ressourcen möglichst immer über Propertys verwalten, wie es die Beispielprojekte dieses Buches auch machen. Sie können die Ressourcen immer freigeben, indem Sie die entsprechende Property auf nil setzen. Falls Sie automatisches Referenzenzählen einsetzen, sollten Sie für alle Outlets auf Nicht-Top-Level-Objekte – außer für die Arrays von Outlet-Collections – Propertys mit dem Speicherverwaltungstyp weak verwenden, da der Controller sie dadurch automatisch mit dem Top-Level-Objekt freigibt.

Top-Level-Objekte und die Arrays von Outlet-Collections müssen Sie hingegen immer halten, indem Sie den Speicherverwaltungstyp strong beziehungsweise retain verwenden. Außerdem müssen Sie für diese Objekte entscheiden, wann der Viewcontroller sie freigeben soll. Hierbei sind insbesondere die Ressourcen des Ladezyklus interessant. Vor der iOS-Version 6 hat Apple empfohlen, sie in der Methode viewDidUnload freizugeben. Diese Methode ist seit iOS 6 indes veraltet, und Sie sollten sie stattdessen in der Methode didReceiveMemoryWarning freigeben, wenn die Methode der Oberklasse den View des Controllers freigegeben hat. Der Code sollte also so aussehen:

- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
if(![self isViewLoaded]) {
// Freigabe aller wiederherstellbaren Ressourcen, die
// der Controller in viewDidLoad erzeugt oder lädt.
}
}

Listing 3.41 Ressourcenfreigabe unter iOS 6

Da es die Methode didReceiveMemoryWarning jedoch schon seit iOS 2 gibt, können Sie das Vorgehen aus Listing 3.41 auch schon für die älteren Betriebssystemversionen einsetzen. Dabei ist die Überprüfung allerdings wichtig. Wenn die App den View des Viewcontrollers während einer Speicherwarnung anzeigt, erhält der Controller zwar die Warnung, aber gibt dennoch nicht seinen View frei.

Tipp

Die Methode didReceiveMemoryWarning sollten Sie immer so implementieren, dass Cocoa Touch sie mehrmals aufrufen kann. Sie können die korrekte Funktionsweise übrigens gut im Simulator testen. Rufen Sie dazu einfach den Menüpunkt HardwareSpeicherwarnhinweis simulieren auf, dann sendet der Simulator eine Speicherwarnung an Ihre App.



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