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

Inhaltsverzeichnis
Geleitwort
Vorwort
1 Hello iPhone
2 Die Reise nach iOS
3 Sehen und anfassen
4 Alles unter Kontrolle
5 Daten, Tabellen und Controller
6 Models, Layer, Animationen
7 Programmieren, aber sicher
8 Datenserialisierung und Internetzugriff
9 Multimedia
10 Jahrmarkt der Nützlichkeiten
Stichwort

Jetzt 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
Rheinwerk Computing
1172 S., geb., mit DVD
49,90 Euro, ISBN 978-3-8362-2734-6
Pfeil 6 Models, Layer, Animationen
Pfeil 6.1 Modell und Controller
Pfeil 6.1.1 iOS Next Topmodel
Pfeil 6.1.2 View an Controller
Pfeil 6.1.3 Gerätebewegungen auswerten
Pfeil 6.1.4 Modell an Controller
Pfeil 6.1.5 Undo und Redo
Pfeil 6.1.6 Unit-Tests
Pfeil 6.2 Als die Views das Laufen lernten
Pfeil 6.2.1 Animationen mit Blöcken
Pfeil 6.2.2 Transitionen
Pfeil 6.2.3 Zur Animation? Bitte jeder nur einen Block!
Pfeil 6.3 Core Animation
Pfeil 6.3.1 Layer
Pfeil 6.3.2 Vordefinierte Layer-Klassen
Pfeil 6.3.3 Der Layer mit der Maske
Pfeil 6.3.4 Unser Button soll schöner werden
Pfeil 6.3.5 Spieglein, Spieglein an der Wand
Pfeil 6.3.6 Der bewegte Layer
Pfeil 6.3.7 Daumenkino
Pfeil 6.3.8 Relativitätstheorie
Pfeil 6.3.9 Der View, der Layer, seine Animation und ihr Liebhaber
Pfeil 6.3.10 Transaktionen
Pfeil 6.3.11 Die 3. Dimension
Pfeil 6.4 Scrollviews und gekachelte Layer
Pfeil 6.4.1 Scrollen und Zoomen
Pfeil 6.4.2 Die Eventverarbeitung
Pfeil 6.4.3 Scharfe Kurven
Pfeil 6.4.4 Ganz großes Kino
Pfeil 6.4.5 PDF-Dateien anzeigen
Pfeil 6.5 Über diese Brücke musst du gehen
Pfeil 6.5.1 Toll-free Bridging und ARC
Pfeil 6.5.2 C-Frameworks und ARC
Pfeil 6.6 Was Sie schon immer über Instruments wissen wollten, aber nie zu fragen wagten
Pfeil 6.6.1 Spiel mir das Lied vom Leak
Pfeil 6.6.2 Ich folgte einem Zombie
Pfeil 6.6.3 Time Bandits
Pfeil 6.6.4 Instruments und der Analyzer

6Models, Layer, AnimationenZur nächsten Überschrift

»Ach, er will doch nur spielen.«
– Unbekannter Hundebesitzer

Animationen sind ein wichtiger, jedoch leider häufig auch unterschätzter Bestandteil einer grafischen Benutzerschnittstelle. Durch Animationen können Sie die Aktionen der Applikation hervorheben und so dem Nutzer eine zusätzliche Rückmeldung geben.

Eine gute Animation hebt die Veränderungen auf dem Bildschirm hervor und verlängert den Wahrnehmungszeitraum für den Nutzer, ohne dabei störend zu wirken. Wenn Sie beispielsweise in der Tabellenansicht des Fototagebuchs einen Eintrag auswählen, dann schiebt der Navigationcontroller die Detailansicht auf den Bildschirm. Diese Animation hebt einerseits den Viewwechsel hervor. Sie erklärt andererseits auch den Zurück-Button in der Detailansicht: Sie sind durch eine Bewegung nach rechts in diese Ansicht gelangt. Also gelangen Sie mit dem Pfeil nach links wieder zurück.

Sie können hingegen Animationen nicht nur für den Wechsel kompletter Screens verwenden, sondern sie auch auf einzelne Views und deren Darstellungsschicht, den Layern, anwenden. In diesem Kapitel lernen Sie Layer und die verschiedenen Animationsmöglichkeiten von Cocoa Touch kennen.

Projektinformation

Den Quellcode des folgenden Beispielprojekts Games finden Sie auf der DVD unter Code/Apps/iOS7/Games oder im Github-Repository zum Buch im Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Auflage_3/Apps/iOS7/Games.

Das Beispielprojekt Games dieses Kapitels enthält zwei einfache Spiele, an denen sich die Funktionsweise von Animationen besonders gut verdeutlichen lässt. Die Spiele kennen Sie wahrscheinlich. Das erste ist ein Schiebepuzzle, bei dem Sie Bildteile auf einer quadratischen Fläche so lange verschieben müssen, bis die Teile zu einem Gesamtbild verschmelzen. Bei dem zweiten Spiel handelt es sich um das bekannte Memory-Spiel.

Die Modelle der Spiele geben weitere Beispiele für die Implementierung eines Modells im Model-View-Controller-Muster. Das Modell des Fototagebuchs ist eher passiv. Seine Hauptaufgabe ist die Speicherung der Daten. Im Gegensatz dazu speichern die Modelle der Spiele nicht nur die Daten, sondern sie müssen den Controller bei Datenänderungen auch informieren.


Rheinwerk Computing - Zum Seitenanfang

6.1Modell und ControllerZur nächsten ÜberschriftZur vorigen Überschrift

Dieser Abschnitt betrachtet die Modellschicht im Model-View-Controller-Muster von einer anderen Seite. Modelle, die auf Core Data basieren, bilden in erster Linie größere Datenmengen gleichartiger Objekte ab. Die Konsistenz der Daten, also ihre Gültigkeit, lässt sich durch relativ wenige und einfache Regeln beschreiben. Beispielsweise muss im Fototagebuch jedes Medium einen Tagebucheintrag haben.


Rheinwerk Computing - Zum Seitenanfang

6.1.1iOS Next TopmodelZur nächsten ÜberschriftZur vorigen Überschrift

Die Modelle zu den Spielen in diesem Kapitel bestehen aus relativ wenigen Daten. Das Modell des Schiebepuzzles besteht beispielsweise nur aus einem Objekt. Andererseits muss es auch die Konsistenz der Spieledaten sicherstellen, und das ist komplizierter als bei vielen Core-Data-Datenmodellen. Das Modell des Schiebepuzzles stellt die Gültigkeit sicher, indem es nur erlaubte Operationen auf den Daten zulässt.

Die Klasse Puzzle im Projekt Games stellt das Modell des Schiebepuzzles dar. Sie verwendet dazu ein C-Array von NSUInteger-Werten. Dabei stellt jeder Wert ein Puzzleteil dar, während die Position eines Wertes im Array die Position des entsprechenden Puzzleteils im Spielfeld angibt.

Abbildung

Abbildung 6.1 Modell des Schiebepuzzles

Das linke Bild in Abbildung 6.1 stellt das gelöste Puzzle – die Ausgangsstellung – dar. Jeder Wert befindet sich dabei an der Position mit dem gleichen Index – also Wert 0 an Position 0, Wert 1 an Position 1 und so weiter. Der Wert 15 repräsentiert das leere Feld, das sich bei der Ausgangsstellung auf der letzten Position befindet.

Das Verschieben der Steine ändert nun die Zuordnung der Werte zu den Positionen. Wenn Sie beispielsweise die Steine entlang des Pfeiles jeweils auf das leere Feld schieben, erhalten Sie die Puzzledarstellung auf der rechten Seite der Abbildung. Die Werte haben dann im Array des Modellobjekts die folgende Anordnung: [0, 1, 2, 3, 4, 15, 6, 7, 8, 5, 9, 11, 12, 13, 10, 14].

Konsistenz des Schiebepuzzles

Das Modell eines Schiebepuzzles ist konsistent, wenn sich die Anordnung der Werte in dessen Array durch beliebige Schiebeoperationen aus der Ausgangsstellung erzeugen lässt. Die Puzzledarstellung [0, 1, 2, 3, 4, 15, 6, 7, 8, 5, 9, 11, 12, 13, 10, 14] (rechtes Bild in Abbildung 6.1) ist also konsistent, da sie sich aus der Ausgangsdarstellung erzeugen lässt.

Ein mögliches Beispiel für ein inkonsistentes Puzzle hat das Array [1, 0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]. Das ist ein Puzzle in der Ausgangsstellung, bei dem die ersten beiden Teile vertauscht sind. Sie können die Teile eines konsistenten Puzzles beliebig oft verschieben, jedoch nie diese Anordnung der Teile erreichen.

Um die Konsistenz des Puzzlemodells sicherzustellen, liegen ihm folgende Regeln zugrunde:

  1. Ein neues Puzzle hat immer die Ausgangsstellung.
  2. Alle Methoden, die die Anordnung der Teile verändern, basieren auf erlaubten Spielzügen.
  3. Alle anderen Methoden lesen die Daten nur aus oder basieren auf Methoden der zweiten Regel.

Oder mit anderen Worten: Die Operationen im Modell entsprechen immer genau den Operationen des wirklichen Schiebepuzzles.

Das Puzzle besitzt eine private Property items, die das Array mit den Werten enthält, und die Länge des Arrays speichert das Modell in der Property size. Die erste Regel lässt sich sehr einfach herstellen. Wenn die Klasse das Array anlegt, setzt sie alle Einträge entsprechend:

NSUInteger theSize = self.size;
for(NSUInteger i = 0; i < theSize; ++i) {
self.items[i] = i;
}
self.freeIndex = theSize – 1;

Listing 6.1 Initialisierung des Arrays des Modells

Im Beispielprogramm finden Sie diese Schleife in der internen Methode clear des Puzzlemodells. Das Puzzle merkt sich in der Property freeIndex außerdem den Index des freien Feldes im Array. Das ist zwar nicht unbedingt notwendig, erleichtert jedoch die Implementierung der anderen Methoden.

Die Puzzleklasse besitzt zwei Methoden, mit denen Sie die Anordnung der Puzzleteile verändern können. Die beiden Methoden bilden die Steuerungsmöglichkeiten des Spiels ab. Sie können das iPhone in vier Richtungen kippen, um die Teile zu bewegen. Diese Steuerungsmöglichkeit implementiert die Methode tiltToDirection:. Sie können außerdem einen Stein berühren und ihn auf das freie Feld ziehen, was die Methode moveItemAtIndex:toDirection: abbildet.

Für die Richtungen verwendet das Modell einen eigenen Aufzählungstyp PuzzleDirection mit fünf möglichen Werten; für jeweils jede Richtung einen. Mit dem Wert PuzzleNoDirection lassen sich auch Zustände ohne spezifische Richtung abbilden.

typedef enum {
PuzzleDirectionUp = 0,
PuzzleDirectionRight,
PuzzleDirectionDown,
PuzzleDirectionLeft,
PuzzleNoDirection
} PuzzleDirection;

Listing 6.2 Aufzählungstyp mit den möglichen Bewegungsrichtungen

Abbildung 6.2 stellt die möglichen Spielzüge des Feldes mit dem Index 6 dar. Das freie Feld befindet sich dabei jeweils in dem Feld, auf das der Pfeil zeigt. Wenn Sie beispielsweise das Puzzle nach oben kippen, dann muss das freie Feld den Index 2 haben. Oder andersherum: Wenn Sie das Puzzle nach oben kippen und der Index des freien Feldes ist 2, dann muss die Methode tiltToDirection: die Felder 2 und 6 miteinander vertauschen.

Daraus können Sie, ausgehend vom freien Feld an der Position freeIndex, die Regeln für das Kippen herleiten:

Tabelle 6.1 Regeln für das Kippen des Puzzles

Kipprichtung Index des Feldes für den Tausch

links

freeIndex + 1

rechts

freeIndex - 1

oben

freeIndex + 4

unten

freeIndex - 4

Abbildung

Abbildung 6.2 Spielzüge im Puzzle

Es gibt natürlich auch ungültige Züge. Nehmen wir an, das freie Feld befindet sich an Position 12, und Sie kippen das Puzzle nach rechts. Nach den Regeln aus Tabelle 6.1 müssten Sie dann die Felder 11 und 12 miteinander vertauschen (gestrichelter Pfeil in Abbildung 6.2). Das ist dennoch kein gültiger Zug, weil der Stein dabei die Zeile und Spalte auf einmal wechselt. Bei einem gültigen Zug müssen also die Indizes des freien Feldes und des Tauschfeldes entweder in der gleichen Zeile oder in der gleichen Spalte liegen.

Den Zeilen- oder Spaltenindex zu einem Feldindex können Sie über eine Division mit Rest mit 4 als Teiler ermitteln. Dazu ein paar Beispiele: Wenn Sie 13 durch 4 mit Rest teilen, erhalten Sie 13 = 3 × 4 + 1 also 3 mit Rest 1 als Ergebnis, und 5 = 1 × 4 + 1 ist 1 mit Rest 1. Da der Divisionsrest bei beiden Rechnungen gleich ist, liegen beide Werte in der gleichen Spalte. Hingegen ist 15 = 3 × 4 + 3 also 3 Rest 3. Die Divisionsreste von 13 und 15 sind zwar unterschiedlich, allerdings ist bei beiden Werten der Quotient 3. Also liegen diese Zahlen in der gleichen Zeile. Wenn Sie das nicht glauben, dann schauen Sie doch in Abbildung 6.2.

Außerdem kann es bei der Anwendung der Regeln aus Tabelle 6.1 passieren, dass der berechnete Index nicht zwischen 0 und 15 liegt. Da das Modell für Indexwerte vorzeichenlose Zahlen vom Typ NSUInteger verwendet, können bei einer Subtraktion jedoch keine negativen Zahlen entstehen. Stattdessen findet ein Überlauf statt – mit einer sehr großen Zahl als Ergebnis. Wenn die Applikation beispielsweise 4 von 3 abzieht, ist das Ergebnis 4.294.967.295. Die Gültigkeit eines Kippzuges des Puzzles überprüft die Klasse Puzzle anhand der beiden Methoden rowOfFreeIndexIsEqualToRowOfIndex: und columnOfFreeIndexIsEqualToColumnOfIndex:, die Sie in Listing 6.3 sehen.

- (BOOL)rowOfFreeIndexIsEqualToRowOfIndex: 
(NSUInteger)inToIndex {
NSUInteger theLength = self.length;
NSUInteger theSize = self.size;
NSUInteger theIndex = self.freeIndex;

return inToIndex < theSize &&
(theIndex / theLength) == (inToIndex / theLength);
}

- (BOOL)columnOfIndexFreeIndexIsEqualColumnOfIndex:
(NSUInteger)inToIndex {
NSUInteger theLength = self.length;
NSUInteger theSize = self.size;
NSUInteger theIndex = self.freeIndex;

return inToIndex < theSize &&
(theIndex % theLength) == (inToIndex % theLength);
}

Listing 6.3 Gültigkeitsprüfung für Spielzüge

Dabei enthält der Parameter inIndex die Position des Feldes für die Vertauschung mit dem leeren Feld. Der Ausdruck self.length liefert die Breite beziehungsweise Höhe des Puzzles – also 4. Wenn eine von beiden Methoden aus Listing 6.3 den Wert YES liefert, vertauscht die Methode tiltToDirection: die Werte im angegebenen Feld und im freien Feld.

Die Logik der Methode moveItemAtIndex:toDirection: ist verglichen mit tiltToDirection: wesentlich einfacher. Der Indexparameter gibt das Feld für die Vertauschung mit dem leeren Feld an. Sie brauchen also nur zu prüfen, ob das leere Feld in der angegebenen Richtung vom angegebenen Feld liegt. Dazu berechnet die Methode den Index des Feldes in der angegebenen Richtung analog zur Methode tiltToDirection:. Wenn dieser Wert mit dem angegebenen Index übereinstimmt, vertauscht die Methode die Werte der beiden Felder.

Das Modell speichert neben den Positionen der Puzzleteile auch die Anzahl der durchgeführten Züge. Dazu stellt die Klasse die nur lesbare Property moveCount zur Verfügung und erhöht den dahinterstehenden Wert bei jedem gültigen Zug in moveItemAtIndex:toDirection: und tiltToDirection: um eins.

Mit der Methode shuffle können Sie das Puzzle durchschütteln. Sie ermittelt dazu mehrmals über die Methode nextIndex eine zufällige Position, auf die sie dann das freie Feld verschiebt. Das iPhone kann zwar keine echten Zufallszahlen erzeugen, allerdings sind die Rückgabewerte der Systemfunktion rand() so schön durcheinandergewirbelt, dass sich der jeweils nächste Wert vom Nutzer nur schwer vorhersagen lässt. Aus diesem Grund nennt man diese Werte auch Pseudozufallszahlen. Die Funktion liefert jedoch nach jedem Programmstart immer die gleiche Zahlenfolge, was beim Puzzle immer zu der gleichen Stellung führen würde. Die App löst dieses Problem, indem Sie in der Methode applicationDidBecomeActive die Funktion srand() mit der aktuellen Uhrzeit über die Anweisung

srand((unsigned) [NSDate timeIntervalSinceReferenceDate]);

aufruft. Dadurch ändert sie jeweils den Anfangszustand für die Berechnung der Pseudozufallszahlen auf einen anderen Wert. Die Methode nextIndex ermittelt so lange ein neues Feld, bis dessen Index ungleich dem Index des freien Feldes ist (siehe Listing 6.4).

- (NSUInteger)nextIndex {
NSUInteger theSize = self.size;
NSUInteger theIndex = rand() % theSize;

while(theIndex == self.freeIndex) {
theIndex = rand() % theSize;
}
return theIndex;
}

Listing 6.4 Berechnung der nächsten Position für die Methode »shuffle«

Die Methode shuffle verwendet die Methode tiltDirectionForIndex: aus Listing 6.5, die zu einer Position eine Kipprichtung berechnet, die das leere Feld näher zum Feld mit der angegebenen Position schiebt. Die Implementierung dieser Methode muss mehrere Fälle überprüfen. Bei wiederholten Aufrufen liefert sie zunächst so lange vertikale Richtungen, bis das freie Feld und die Position in einer Zeile liegen, und danach liefert sie horizontale Richtungen, bis beide Positionen übereinstimmen. Falls die übergebene Position schon mit der Position des freien Feldes übereinstimmt, liefert die Methode tiltDirectionForIndex: den Wert PuzzleNoDirection zurück, um anzuzeigen, dass keine Feldvertauschung notwendig ist.

- (PuzzleDirection)tiltDirectionForIndex:(NSUInteger)inIndex {
NSUInteger theFreeIndex = self.freeIndex;

if(inIndex == theFreeIndex) {
return PuzzleNoDirection;
}
else if([self rowOfFreeIndexIsEqualToRowOfIndex:
inIndex]) {
return inIndex < theFreeIndex ?
PuzzleDirectionRight : PuzzleDirectionLeft;
}
else {
return inIndex < theFreeIndex ?
PuzzleDirectionDown : PuzzleDirectionUp;
}
}

Listing 6.5 Berechnung einer Kipprichtung zu einer Position

Diese wiederholten Aufrufe führt die Methode shuffle aus. Sie ermittelt zunächst eine Position über nextIndex und schiebt danach das freie Feld durch mehrfache Aufrufe der Methode tiltToDirection: auf das Feld mit diesem Index. Das macht sie mehrmals, so dass das Puzzle danach schön durcheinandergewürfelt, aber trotzdem lösbar ist. Dabei ist die Lösbarkeit dadurch garantiert, dass dieses Vorgehen nur erlaubte Operationen nach den oben genannten Regeln ausführt. Die Methode shuffle erhält also die Konsistenz des Puzzles.

- (void)shuffle {
NSUInteger theSize = self.size;

for(NSUInteger i = 0; i < 4 * theSize; ++i) {
NSUInteger theShuffleIndex = self.nextIndex;
PuzzleDirection theDirection =
[self tiltDirectionForIndex:theShuffleIndex];

while(theDirection != PuzzleNoDirection) {
[self tiltToDirection:theDirection
withCountOffset:0];
theDirection = [self
tiltDirectionForIndex:theShuffleIndex];
}
}
self.moveCount = 0;
}

Listing 6.6 Schütteln des Puzzles


Rheinwerk Computing - Zum Seitenanfang

6.1.2View an ControllerZur nächsten ÜberschriftZur vorigen Überschrift

Das Puzzlespiel bietet zwei Möglichkeiten, die Steine zu verschieben. Zum einen können Sie die Steine per Finger verschieben. Das realisiert die App über vier Gesture-Recognizer, die Sie über den Interface Builder zu dem Puzzleview hinzufügen. Die Auswertung der Swipe-Gesten erfolgt dabei über jeweils eine Methode pro Richtung und die Hilfsmethode handleGestureRecognizer:withDirection:, die Sie in Listing 6.7 finden.

- (void)handleGestureRecognizer:
(UIGestureRecognizer *)inRecognizer
withDirection:(PuzzleDirection)inDirection {
UIView *thePuzzleView = self.puzzleView;
Puzzle *thePuzzle = self.puzzle;
CGPoint thePoint =
[inRecognizer locationInView:thePuzzleView];
NSUInteger theLength = thePuzzle.length;
CGSize theViewSize = thePuzzleView.frame.size;
NSUInteger theRow =
thePoint.y * theLength / theViewSize.height;
NSUInteger theColumn =
thePoint.x * theLength / theViewSize.width;
NSUInteger theIndex = theRow * theLength + theColumn;

[thePuzzle moveItemAtIndex:theIndex
toDirection:inDirection];
}

- (void)handleLeftSwipe:
(UISwipeGestureRecognizer *)inRecognizer {
[self handleGestureRecognizer:inRecognizer
withDirection:PuzzleDirectionLeft];
}

- (void)handleRightSwipe:
(UISwipeGestureRecognizer *)inRecognizer {
[self handleGestureRecognizer:inRecognizer
withDirection:PuzzleDirectionRight];
}

- (void)handleUpSwipe:
(UISwipeGestureRecognizer *)inRecognizer {
[self handleGestureRecognizer:inRecognizer
withDirection:PuzzleDirectionUp];
}

- (void)handleDownSwipe:
(UISwipeGestureRecognizer *)inRecognizer {
[self handleGestureRecognizer:inRecognizer
withDirection:PuzzleDirectionDown];
}

Listing 6.7 Auswertung der Swipe-Gesten

Die Methode handleGestureRecognizer:withDirection: ermittelt zunächst über die Methode locationInView: des Gesture-Recognizers die Koordinaten der Berührung im Puzzleview, und aus den Koordinaten bestimmt sie die Zeile und die Spalte des Felde im Puzzle. Aus diesen beiden Werten kann sie dann den entsprechenden Index des Feldes berechnen. Mit der übergebenen Richtung ruft sie dann die Methode moveItemAtIndex:toDirection: auf.


Rheinwerk Computing - Zum Seitenanfang

6.1.3Gerätebewegungen auswertenZur nächsten ÜberschriftZur vorigen Überschrift

Sie können auch die Puzzlesteine über Kippbewegungen des Gerätes verschieben. Dafür verwendet die App den Beschleunigungssensor, auf den Sie über die Klasse CMMotionManager zugreifen können. Damit Sie diese Klasse verwenden können, müssen Sie das Core-Motion-Framework über die Target-Einstellungen einbinden. Dazu klicken Sie unter Linked Frameworks and Libraries den Plus-Button an, wählen den Eintrag CoreMotion.framework aus und drücken den Button Add (siehe Abbildung 6.3).

Abbildung

Abbildung 6.3 Hinzufügen des Core-Motion-Frameworks

Apple empfiehlt, jeweils höchstens einen Motionmanager pro Applikation zu erzeugen. In der Games-App verwendet zwar nur das Puzzle dieses Objekt, dennoch ist es eine gute Idee, es zentral über das App-Delegate zu verwalten, das dafür die Property motionManager besitzt. Die Initialisierung erfolgt in der Methode application:didFinishLaunchingWithOptions:, die auch das Aktualisierungsintervall des Managers auf eine Zehntelsekunde festlegt.

self.motionManager = [CMMotionManager new];
[self.motionManager setAccelerometerUpdateInterval:0.1];

Listing 6.8 Erzeugung und Initialisierung des Beschleunigungssensors

Die Kategorie UIViewController(Games) stellt über die Methode motionManager den Motionmanger den Viewcontrollern zur Verfügung. Der Motionmanager liefert die Werte des Beschleunigungssensors über den Aufruf eines Blocks an die Applikation, die Sie beim Start an die Methode startAccelerometerUpdatesToQueue:withHandler: als zweiten Parameter übergeben. Der erste Parameter ist eine Operationqueue, in der der Funktionsaufruf erfolgt. Sie können hier einfach die Haupt-Queue verwenden.

- (void)viewDidAppear:(BOOL)inAnimated {
[super viewDidAppear:inAnimated];
CMMotionManager *theManager = self.motionManager;

[theManager startAccelerometerUpdatesToQueue:
[NSOperationQueue mainQueue]
withHandler:^(CMAccelerometerData *inData,
NSError *inError) {
if(inData == nil) {
NSLog(@"error: %@", inError);
}
else {
[self handleAcceleration:inData.acceleration];
}
}];
}

Listing 6.9 Starten der Beschleunigungssensorabfragen

Das Stoppen der Abfragen erfolgt in der Methode viewWillDisapear: über einen Aufruf der Methode stopAccelerometerUpdates.

- (void)viewWillDisappear:(BOOL)inAnimated {
CMMotionManager *theManager = self.motionManager;

[theManager stopAccelerometerUpdates];
[super viewWillDisappear:inAnimated];
}

Listing 6.10 Stoppen der Beschleunigungssensorabfragen

Der Beschleunigungssensor liefert die Werte in einem Objekt der Klasse CMAccelerometerData an die Funktion. Dieses Objekt liefert über die Property acceleration ein Datum der Struktur CMAcceleration, das drei Fließkommawerte entlang der Hauptachsen x, y und z enthält (siehe Abbildung 6.4).

Abbildung

Abbildung 6.4 Die Achsen eines Acceleration-Objekts

Die Werte für diese Achsen geben dabei deren Ausrichtung zur Erdmitte an. Wenn das iPhone mit dem Display nach oben horizontal auf dem Tisch liegt, liefert der Sensor im Idealfall die Werte x = 0, y = 0 und z = 0 –1. Halten Sie hingegen das Telefon wie in Abbildung 6.4 genau senkrecht, beispielsweise um ein Foto zu schießen, dann erhalten Sie die Werte x = 0, y = –1, z = 0. Es hat also immer diejenige Achse einen Wert von +/–1, die nach unten zeigt, wobei das Vorzeichen dem Vorzeichen an der Achsenbeschriftung entspricht.

Do it yourself

Apple stellt das Beispielprogramm MotionGraphs mit der Dokumentation zur Verfügung. Damit können Sie sich die Werte des Beschleunigungssensors auf Ihrem iPhone anzeigen lassen. Dieses Programm ist sehr praktisch, wenn Sie eigene Programme mit Beschleunigungssensor-Unterstützung entwickeln wollen.

Um es in Xcode zu öffnen, rufen Sie die Hilfe über den Menüpunkt HelpDocumentation and API Reference oder alt+cmd+? (beziehungsweise ª+alt+cmd+ß auf einer deutschen Tastatur) auf und geben in das Suchfeld »MotionGraphs« ein. Alternativ können Sie das Projekt auch über die URL https://developer.apple.com/library/ios/samplecode/MotionGraphs öffnen. Diese App ist allerdings nur auf einem iOS-Gerät sinnvoll, da der Simulator keinen Beschleunigungssensor besitzt und diesen auch nicht nachahmen kann. Über die Tabbar dieser App können Sie die verschiedenen Sensoren des Motionmanagers auswählen; den Beschleunigungssensor aktivieren Sie über den mittleren Reiter Accelerometer.

Die Messwerte des Beschleunigungssensors wertet die Methode handleAcceration: folgendermaßen aus: Wenn Sie das Gerät aus der horizontalen Lage in eine Richtung kippen, schiebt die App den passenden Stein auf das freie Feld. Danach müssen Sie das Gerät erst wieder in die Ausgangslage bringen, um den nächsten Stein verschieben zu können. Um das zu verwirklichen, merkt sich der Controller die letzte Kipprichtung in der privaten Property lastDirection. Neben den vier Richtungen für oben, unten, links und rechts gibt es ja noch einen Wert für keine Richtung namens PuzzleNoDirection. Nur wenn die letzte Kipprichtung diesen Wert hat, führt der Controller einen Spielzug aus.

- (void)handleAcceleration:(CMAcceleration)inAcceleration {
float theX = inAcceleration.x;
float theY = inAcceleration.y;

if(self.lastDirection == PuzzleNoDirection) {
Puzzle *thePuzzle = self.puzzle;

if(fabs(theX) > kHorizontalMaximalThreshold) {
self.lastDirection = theX < 0 ?
PuzzleDirectionLeft : PuzzleDirectionRight;
}
else if(fabs(theY) > kVerticalMaximalThreshold) {
self.lastDirection = theY < 0 ?
PuzzleDirectionDown : PuzzleDirectionUp;
}
[thePuzzle tiltToDirection:self.lastDirection];
}
else if(fabs(theX) < kHorizontalMinimalThreshold &&
fabs(theY) < kVerticalMinimalThreshold) {
self.lastDirection = PuzzleNoDirection;
}
}

Listing 6.11 Auswertung der Beschleunigungssensorwerte

Für die Auswertung sind nur die x- und y-Werte interessant. Sie lassen sich direkt in Links/rechts- beziehungsweise Unten/oben-Bewegungen übersetzen. Abbildung 6.5 veranschaulicht diese Auswertung. Wenn der x- und der y-Wert im grauen Quadrat in der Mitte liegen und somit das Gerät nicht weit genug gekippt wurde, dann setzt die Methode den Property-Wert lastDirection auf PuzzleNoDirection. Die Methode setzt den Property-Wert jeweils auf die Konstante, in deren Bereich sich der x- und der y-Wert befindet. Außerdem sendet sie diese Richtung natürlich auch über die Methode tiltToDirection: als Kippbewegung an das Puzzle. Nur bei dem weißen Bereich um das graue Quadrat verändert die Methode den Property-Wert nicht.

Abbildung

Abbildung 6.5 Auswertungsbereiche für Beschleunigungswerte


Rheinwerk Computing - Zum Seitenanfang

6.1.4Modell an ControllerZur nächsten ÜberschriftZur vorigen Überschrift

Der Viewcontroller übersetzt also alle Eingaben der Gesture-Recognizer und des Beschleunigungssensors in Methodenaufrufe des Modells. Er muss allerdings nicht nur das Modell, sondern auch den View aktualisieren. Es wäre naheliegend, wenn Sie dazu in den Controller entsprechende Methodenaufrufe für den View einfügten. Dieses Vorgehen würde jedoch zu Methoden mit einem sehr ähnlichen Aufbau führen. Der erste Schritt aktualisiert das Modell und der zweite den View, was jedoch einige Nachteile hat:

  1. Durch den ähnlichen Aufbau entsteht die Gefahr von Code-Doppelungen, und mit der Zeit fängt der Code an, zu riechen [Anm.: Siehe http://de.wikipedia.org/wiki/Code_smells.] .
  2. Sie können komplexere Veränderungen des Modells unter Umständen nur sehr schlecht über dieses Vorgehen abbilden. Die Methode shuffle führt beispielsweise sehr viele Vertauschungsoperationen durch.
  3. Wenn nicht nur ein, sondern mehrere Controller das Modell verändern können, können der Modellinhalt und die Viewdarstellung voneinander differieren.

Diese Probleme lassen sich vermeiden, wenn das Modell den View automatisch über die Veränderungen benachrichtigt. Das Modell darf indes auf keinen Fall eine Abhängigkeit zum Controller oder View haben, weswegen Sie vom Modell nicht einfach auf diese Schichten zugreifen können. Außerdem sollte ja das Modell beliebig viele Controller und Views über Zustandsänderungen informieren können.

Stattdessen kann auch das Modell bei jeder Veränderung entsprechende Benachrichtigungen versenden. Die vom Modellzustand abhängigen Viewcontroller lauschen auf diese Benachrichtigungen und aktualisieren sich und den View entsprechend.

Das Modell des Schiebepuzzles versendet zwei Benachrichtigungen mit jeweils gleich aufgebautem Info-Dictionary. Die Methode tiltToDirection: verschickt die Benachrichtigung kPuzzleDidTiltNotification, während moveItemAtIndex:toDirection: die Benachrichtigung kPuzzleDidMoveNotification versendet. Das Directory userInfo in der Benachrichtigung enthält dabei die folgenden Schlüssel:

Tabelle 6.2 Schlüssel des User-Info-Dictionarys

Schlüssel Wert

kPuzzleDirectionKey

die Bewegungsrichtung des Puzzleteils

kPuzzleFromIndexKey

der ursprüngliche Index des Puzzleteils

kPuzzleToIndexKey

der neue Index des Puzzleteils

Die Werte in der Tabelle haben alle den Typ NSUInteger. Die muss sie jedoch in NSNumber-Objekte kapseln, um sie in einem NSDictionary verwenden zu können.

Abbildung 6.6 stellt das Vorgehen zur Aktualisierung des Views grafisch dar. Wenn der Nutzer eine Eingabe macht, läuft die Verarbeitung vom View über den Viewcontroller ins Modell. Das Modell schickt dann eine Benachrichtigung, die genau den umgekehrten Weg nimmt. Dieses Vorgehen erinnert ein bisschen an das Spielen über Bande beim Billard und wirkt umständlich. Der Vorteil dabei ist jedoch, dass der View keine Änderung des Modells verpassen kann. Wenn beispielsweise ein anderer Controller – symbolisiert durch das Fragezeichen – das Modell verändert, benachrichtigt es immer den View. Der View passt sich also immer dem Modell an.

Abbildung

Abbildung 6.6 Aktualisierung des Views über Modellaktualisierungen

Das Modell speichert außerdem die Anzahl der Züge. Auch hier soll die Anzeige des Spielstands automatisch bei einer Änderung erfolgen. Das Modell könnte hierzu auch Benachrichtigungen verwenden. Da es hierbei jedoch um die Beobachtung eines einzelnen Wertes geht, ist hierfür Key-Value-Observing (KVO) besser geeignet.

Key-Value-Observing hat gegenüber Benachrichtigungen den Vorteil, dass Sie dafür nichts am Modell ändern müssen. Die Möglichkeit, Werte eines Objekts zu beobachten, ist bei den beobachteten Objekten in Cocoa sozusagen schon eingebaut. Sie müssen nur noch den Beobachter einrichten. Das machen Sie über die Methode addObserver:forKeyPath:options:context:.

Der PuzzleViewController registriert sich beim Puzzlemodell als Beobachter für die Property moveCount über den folgenden Aufruf:

[self.puzzle addObserver:self forKeyPath:@"moveCount" 
options:0 context:nil];

Listing 6.12 Registrierung als Beobachter eines Wertes

Bei jeder Änderung der Property moveCount ruft dann Cocoa automatisch die Methode observeValueForKeyPath:ofObject:change:context: des Beobachters auf. Dabei enthalten die ersten beiden Parameter den Namen der beobachteten Eigenschaft (hier moveCount) beziehungsweise das beobachtete Objekt (also das Puzzle). Der Parameter change enthält ein Dictionary mit verschiedenen Werten der Property. Sie können darüber beispielsweise den Wert vor der Änderung ermitteln. Dazu müssen Sie allerdings bei der Registrierung im Parameter options den Wert NSKeyValueObservingOptionOld angeben. Das Dictionary enthält diesen Wert dann unter dem Schlüssel NSKeyValueChangeOldKey.


Rheinwerk Computing - Zum Seitenanfang

6.1.5Undo und RedoZur nächsten ÜberschriftZur vorigen Überschrift

Beim Lösen eines Puzzles machen Sie sicherlich den einen oder anderen Zug, den Sie am liebsten sofort wieder zurücknehmen möchten. Sie können natürlich das zuletzt bewegte Teil wieder zurückschieben. Allerdings erhöht das Modell für diese Rücknahme auch den Zugzähler. Das Puzzle soll indes in dieser Situation auch ein Auge zudrücken können und dem Nutzer die Rücknahme seines letzten Zuges erlauben. Es ist in dieser Hinsicht sogar sehr großzügig; Sie dürfen beliebig viele Züge zurücknehmen.

Das Foundation-Framework stellt für diesen Zweck die Klasse NSUndoManager bereit, mit der Sie eine Verwaltung für Undo und Redo implementieren können. Sie müssen dazu bei jedem Spielzug einen Methodenaufruf registrieren, der diesen Spielzug zurücknimmt.

Methodenaufrufe speichern

Der Undo-Manager merkt sich Methodenaufrufe für die Undo-Operationen, wobei er sie natürlich nicht ausführt. Für das Merken verwendet er Objekte der Klasse NSInvocation, die einen Empfänger, einen Selektor und die Parameter eines Methodenaufrufs speichern kann. Die Methode invoke führt diesen Methodenaufruf aus, der in einem Invocation-Objekt enthalten ist.

Für die Registrierung von Undo-Operationen stellt der Undo-Manager zwei Methoden zur Verfügung. Wenn Sie eine Methode mit nur einem Parameter registrieren möchten, können Sie dazu die Methode registerUndoWithTarget:selector:object: verwenden. Sie erhält den Empfänger, den Selektor und das Parameterobjekt als Parameter.

Die registrierten Methodenaufrufe verwaltet der Undo-Manager intern über einen Stapel (Last-In-First-Out, kurz LIFO) oder auch Undo-Stack. Beispielsweise können Sie damit folgendermaßen die Undo-Operation für einen Setter-Aufruf registrieren:

- (void)setTitle:(NSString *)inTitle {
if(title != inTitle) {
[self.undoManager registerUndoWithTarget:self
selector:@selector(setTitle:) object:title];
title = [inTitle copy];
}
}

Listing 6.13 Setter mit Undo-Manager

Der Setter registriert im Undo-Manager einen Setter-Aufruf mit dem alten Property-Wert. Sie können den Manager durch einen Aufruf der Methode undo dazu veranlassen, die zuletzt registrierte Undo-Operation auszuführen. Wenn das der Setter aus Listing 6.13 war, dann ruft der Undo-Manager erneut diesen Setter auf. Hierbei übergibt er jedoch den alten Wert, so dass das Objekt wieder den Titel vor dem ersten Setter-Aufruf hat.

Zu dem Zeitpunkt, an dem der Setter die Undo-Operation aufruft, registriert er einen neuen Methodenaufruf. Dadurch kommt jedoch der Undo-Stack durcheinander! Aus dieser Not hat Apple eine Tugend gemacht. Während der Ausführung von Undo-Operationen zeichnet der Undo-Manager alle Methodenregistrierungen als Redo-Operationen auf. Eine Redo-Operation macht eine Undo-Anweisung rückgängig, und Sie können sie durch die Methode redo im Undo-Manager ausführen. Der Undo-Manager verwaltet die Redo-Operationen über einen eigenen Stapel – den Redo-Stack.

In Abbildung 6.7 ist die Interaktion des Setters aus Listing 6.13 mit dem Undo-Manager abgebildet. Der dargestellte Ablauf entspricht dabei den folgenden Programmanweisungen:

[theObject setTitle:@"Neu"];
[theObject.undoManager undo];
[theObject.undoManager redo];

Listing 6.14 Programmablauf zu Abbildung 6.7

Die gestrichelten Pfeile in der Abbildung stellen die Registrierung der Undo- und Redo-Operationen dar. Die durchgezogenen Pfeile sind mit der aufgerufenen Methode des Undo-Managers beschriftet und zeigen die Herkunft des ausgeführten Methodenaufrufs an.

Abbildung

Abbildung 6.7 Interaktion des Setters mit dem Undo-Manager

Um nun den Undo-Manager in das Puzzlemodell zu integrieren, muss jede Kippoperation jeweils die entsprechende Kippoperation in die Gegenrichtung beim Undo-Manager registrieren. Allerdings erhöht die Methode tiltToDirection: den Zugzähler dabei immer um eins. Sie brauchen also eine Methode, die wie tiltToDirection:, die Puzzleteile verschiebt, den Zugzähler dagegen um eins verringert. Anstatt die Methode zu kopieren und abzuändern, verwendet das Puzzle die interne Hilfsmethode tiltToDirection:withCountOffset:. Den Wert des zweiten Parameters addiert die Methode zu dem Zugzähler. Sie können also hier den Wert 1 bei normalen Spielzügen und –1 bei Undo-Operationen angeben.

Diese Methode können Sie hingegen nicht über die Methode registerUndoWithTarget:selector:object: beim Undo-Manager registrieren, da sie erstens zwei Parameter und zweitens einfache Datentypen verwendet. Es gibt allerdings eine weitere Möglichkeit, Undo-Methoden zu registrieren. Wenn Sie jetzt denken, dass Sie das Invocation-Objekt selbst bauen müssen, dann sind Sie jedoch gehörig auf dem Holzweg. Der Undo-Manager stellt die Methode prepareWithInvocationTarget: zur Verfügung. Sie können diese Methode mit dem Methodenempfänger aufrufen und an das Ergebnis den Methodenaufruf für die Undo-Operation senden; dadurch lässt sich die Registrierung aus Listing 6.13 so schreiben:

[[self.undoManager prepareWithInvocationTarget:self] 
setTitle:title];

Listing 6.15 Alternative Registrierungsmöglichkeit einer Undo-Operation

Auch dieser Code führt nicht die Methode setTitle: aus. Was auf den ersten Blick wie Zauberei aussieht, ist bei genauerem Hinsehen allerdings nur ein geschickter Trick – wie das bei Magiern ja auch meistens der Fall ist.

Des Rätsels Lösung liegt in der Antwort auf die Frage, was passiert, wenn ein Objekt eine ihm unbekannte Methode empfängt. Das Laufzeitsystem ruft in diesem Fall die Methode forwardInvocation: des Objekts auf, die ein Invocation-Objekt für den fehlerhaften Methodenaufruf als Parameter übergeben bekommt.

Der Undo-Manager nutzt diesen Umstand aus. In der Methode prepareWithInvocationTarget: merkt er sich einfach das Target-Objekt und gibt sich selbst zurück. Außerdem überschreibt er forwardInvocation:. Darin ersetzt er im Invocation-Objekt das Invocation-Target und legt das Objekt auf den Undo- beziehungsweise Redo-Stack. Das Ganze klingt sehr kompliziert, ist jedoch relativ einfach; eine Implementierung könnte beispielsweise folgendermaßen aussehen:

- (id)prepareWithInvocationTarget:(id)inTarget {
self.preparedTarget = inTarget;
return self;
}
- (void)forwardInvocation:(NSInvocation *)inoutInvocation {
[inoutInvocation setTarget:self.preparedTarget];
self.preparedTarget = nil;
// inoutInvoction auf Undo- oder Redo-Stack legen
}

Listing 6.16 Invocation-Erzeugung über Proxyaufruf

Mit der Methode tiltToDirection:withCountOffset: können Sie jetzt die vollständige Undo- und Redo-Funktionalität des Puzzles implementieren. Dabei erfolgt die Registrierung der Undo-Operation folgendermaßen:

id theProxy = [self.undoManager 
prepareWithInvocationTarget:self];
...
[theProxy tiltToDirection:theReverseDirection
withCountOffset:-inOffset];

Listing 6.17 Registrierung der Undo-Operation im Puzzle

Proxys

Dieses Vorgehen basiert auf dem Proxymuster. Ein Proxy ist ein Objekt, das ein anderes Objekt kapselt und dessen Methodenaufrufe entgegennimmt. Der Proxy ist dabei für den Methodenaufrufer vollkommen transparent. Durch das Proxyobjekt besteht die Möglichkeit, die Methodenaufrufe zu modifizieren. Im Fall des Undo-Managers ist das die Speicherung der Methodenaufrufe.

Vielleicht kennen Sie ja den Begriff Proxy von den Netzwerkeinstellungen in OS X oder von Ihrem Internetbrowser. Dort können Sie Ihre Webseitenaufrufe durch einen Proxyserver leiten. Der Proxyserver speichert die aufgerufenen Seiten, um den Traffic zu mindern und die Seitenaufrufe zu beschleunigen. [Anm.: Meistens machen die Web-Proxys im Gegensatz zum Entwurfsmuster jedoch einfach auch nur viel Ärger.] Das ist das gleiche Prinzip wie bei dem Entwurfsmuster.

Durch das Negieren des Offsets verhält sich die Methode nicht nur bei Undo-, sondern auch bei Redo-Aufrufen richtig. Denn Letzteres muss ja den Zugzähler wieder erhöhen.


Rheinwerk Computing - Zum Seitenanfang

6.1.6Unit-TestsZur vorigen Überschrift

Sie haben jetzt für das Puzzle ein Modell, das auf theoretischen Überlegungen zu dem Spiel beruht. Aber macht es denn auch das, was es soll? Normalerweise testen Sie während der Programmerstellung komplette Funktionen Ihrer App. Sie kippen beispielsweise das iPhone und überprüfen, ob die Puzzle-App auch das richtige Teil verschiebt. Diese Funktionstests sind sehr wichtig, und Sie kommen nicht um sie herum.

Andererseits führen Sie diese Tests in der Regel manuell aus. Wenn Sie keinen detaillierten Testplan haben, führt das schnell dazu, Testfälle zu vergessen oder zu schludern. Einen detaillierten Testplan immer komplett durchzuarbeiten, ist jedoch häufig sehr ineffizient. Die Ursachen vieler Programmfehler beruhen allerdings in vielen Fällen auf Fehlern des Modells, und wenn Sie eine effiziente Möglichkeit besitzen, seine Fehler zu finden, trägt das wesentlich zur Stabilität des Programms bei.

Funktionstests

Sie sollten Ihre App vor der Veröffentlichung von mehreren anderen Nutzern testen lassen. Tester finden häufig die erstaunlichsten Fehler in den Apps. Optimalerweise lassen Sie Ihre App nicht nur von verschiedenen Personen, sondern auch auf verschiedenen Geräten überprüfen.

Ein guter Testplan kann dabei übrigens sehr hilfreich sein. Er beschreibt Anwendungsfälle Ihres Programms mit den erwarteten Ergebnissen. Solche Pläne sollten Sie inkrementell erweitern; das heißt, Sie entwickeln aus erkannten Fehlern des Programms neue Testfälle, die ein Wiederauftreten dieser Fehler anzeigen.

Es ist bei komplexen Programmen inzwischen üblich, automatisierte Testverfahren zu erstellen und regelmäßige Testläufe durchzuführen. Xcode 5 unterstützt die Erstellung und Ausführung von Modul- oder auch Unit-Tests erheblich, indem es beim Anlegen eines neuen Projekts automatisch ein Target für solche Tests anlegt. Dieses Target enthält eine Klasse, im Beispielprojekt GamesTests, in die Sie Ihre Testmethoden schreiben können.

Sie können das Target für die Tests indes auch noch später erstellen oder in Ihrem Programm auch mehrere Test-Targets anlegen. Wählen Sie dazu den Menüpunkt FileNew Target... aus. Es erscheint der in Abbildung 6.8 dargestellte Dialog. Selektieren Sie dort in der linken Spalte unter iOS den Punkt Other und dann in der Übersicht das Template Cocoa Touch Unit Testing Bundle.

Abbildung

Abbildung 6.8 Anlegen eines neuen Targets

Wenn Sie auf den Button Next des Dialogs klicken, erscheint der in Abbildung 6.9 dargestellte Dialog. Dort können Sie den Namen und weitere Optionen des neuen Targets festlegen. Nachdem Sie auf den Button Finish geklickt haben, enthält das Projekt eine neue Gruppe mit dem Namen des Targets. In der Gruppe finden Sie eine Klasse, die ebenfalls den Namen des Targets hat und die Oberklasse XCTestCase besitzt.

Die Klasse enthält bereits die drei Methoden:

  1. Das Testframework ruft die Methode setUp jeweils vor der Ausführung jeder Testmethode auf. Sie sollten innerhalb von setUp durch [super setUp]; immer als Erstes die Methode in der Oberklasse aufrufen. Danach können Sie Ihre Testklasse für den Test initialisieren.
  2. Die Methode tearDown ruft das Testframework nach der Ausführung einer Testmethode auf. Sie sollte immer als letzte Anweisung [super tearDown]; enthalten. In dieser Methode können Sie die Ressourcen Ihrer Testklasse wieder freigeben.
  3. Alle Testmethoden beginnen mit dem Präfix test und haben keine Parameter. Die Methode testExample ist ein Beispiel für einen korrekten Namen einer Testmethode.

Abbildung

Abbildung 6.9 Eingabe der Target-Optionen

Sie können in eine Testmethode beliebigen lauffähigen Code schreiben. Sie müssen jedoch alle Klassen Ihres Programms, die Sie testen wollen, zu diesem neuen Target hinzufügen. Dazu klicken Sie im Dateiinspektor der Implementierungsdatei unter der Rubrik Target Membership einfach nur das entsprechende Target an (siehe Abbildung 6.10). Das Gleiche gilt natürlich auch für die verwendeten Frameworks.

Abbildung

Abbildung 6.10 Datei zum Target mit den Unit-Tests hinzufügen

Zusätzlich stellt das XCTest-Framework, auf dem die Testumgebung basiert, Makros bereit, um die Anweisungen zu testen. Sie können mit dem Makro XCTAssertTrue(Bedingung, Fehlermeldung, ...) prüfen, ob die angegebene Bedingung wahr ist. Falls sie falsch ist, gibt das Makro die Fehlermeldung aus. In der Meldung können Sie die üblichen Platzhalter verwenden, die Sie auch von stringWithFormat: kennen. Falls Sie stattdessen erwarten, dass die Bedingung falsch ist, können Sie XCTAssertFalse(Bedingung, Fehlermeldung, ...) verwenden.

Das Testprojekt nutzt die Unit-Tests, um die Modellklassen zu prüfen. Als Grundlage der Tests dient das logische Modell der Puzzleklasse. Der erste Test überprüft die Konstruktion eines neuen Puzzlemodells. Wenn Sie ein neues Modell erzeugen, soll es die Ausgangsstellung haben. Damit die Testklasse das Modell nach den Tests immer freigeben kann, besitzt sie eine Property puzzle, die das Modell der Tests enthält.

Die Testmethode in Listing 6.18 überprüft zunächst, ob das Puzzle die richtige Länge und die richtige Größe besitzt. Das Puzzle befindet sich in der Ausgangsstellung, wenn sich jedes Puzzleteil an der Position befindet, die seinem Wert entspricht. Zur Überprüfung verwendet sie dabei das Makro XCTAssertTrue. Das Makro XCTAssertEqual(linker Wert, rechter Wert, Fehlermeldung, ...) prüft über den Gleichheitsoperator ==, ob der linke gleich dem rechten Wert ist. Sie können damit also nur die Werte einfacher Datentypen wie zum Beispiel int, NSUInteger oder double vergleichen. Allerdings findet dabei keine automatische Typumwandlung statt, und so schlagen die Tests

XCTAssertEqual(1, 1U, @"Fehler"); // Falsch: int mit unsigned
XCTAssertEqual(1, 1.0, @"Fehler"); // Falsch: int mit double

immer fehl.

Im Gegensatz dazu vergleicht das Testmakro XCTAssertEqualObjects(linkes Objekt, rechtes Objekt, Fehlermeldung, ...) die beiden Objekte über die Methode isEqual:.

@implementation GamesTests

- (void)setUp {
[super setUp];
self.puzzle = [Puzzle puzzleWithLength:4];
}

- (void)tearDown {
self.puzzle = nil;
[super tearDown];
}

- (void)testCreation {
XCTAssertTrue(self.puzzle.length == 4,
@"invalid length = %d", self.puzzle.length);
XCTAssertTrue(self.puzzle.size == 16,
@"invalid size = %d", self.puzzle.size);
for(NSUInteger i = 0; i < self.puzzle.size; ++i) {
NSUInteger theValue = [self.puzzle valueAtIndex:i];

XCTAssertEqual(theValue, i,
@"invalid value %d at index %d", theValue, i);
}
}

@end

Listing 6.18 Unit-Test für die Ausgangsstellung des Puzzles

Die Tests lassen sich in Xcode über den Run-Button in der Werkzeugleiste starten. Klicken Sie dazu auf diesen Button, und halten Sie ihn gedrückt, bis ein Pop-over-Menü erscheint. Darin wählen Sie den Punkt Test aus, um Ihre Tests zu starten (siehe Abbildung 6.11).

Die Auswahl von Test ändert außerdem dauerhaft die Darstellung und Beschriftung des Buttons. Für eine wiederholte Ausführung der Tests müssen Sie ihn dann nur noch kurz anklicken, und durch einen langen Klick und entsprechende Auswahl aus dem Menü wechseln Sie wieder zu Run zurück.

Abbildung

Abbildung 6.11 Ausführen der Unit-Tests

Unter Umständen führt Xcode nach dem Auslösen von Test aber keine Tests aus, sondern meldet sich mit einer Alertbox, die besagt, dass Ihr Target Games noch nicht für Tests konfiguriert ist (siehe Abbildung 6.12). In diesem Fall klicken Sie auf den Button Edit Scheme... Im folgenden Dialog sehen Sie eine Liste der Targets, die Xcode als Tests für das Target Games ausführt (siehe Abbildung 6.13). Sie ist in diesem Fall allerdings leer, da Xcode ja ansonsten nicht die Alertbox angezeigt hätte.

Abbildung

Abbildung 6.12 Meldung für nicht konfigurierte Tests

Durch Klicken auf den Plus-Button unterhalb der Liste können Sie unter den vorhandenen Test-Targets des Projekts diejenigen auswählen, die Xcode beim Drücken des Buttons Test in der Werkzeugleiste ausführen soll.

Abbildung

Abbildung 6.13 Anzeige und Auswahl der Test-Targets

Die Liste enthält dabei aber nur die Targets, die Sie als Test-Targets angelegt haben. Wählen Sie die gewünschten Targets so aus, dass der Dialog sie hervorhebt, und klicken Sie dann auf Add (siehe Abbildung 6.14). Danach sollte die Liste aus Abbildung 6.13 die ausgewählten Targets enthalten.

Abbildung

Abbildung 6.14 Auswahl eines Targets als Test

Xcode zeigt die nicht erfüllten Testbedingungen nach der Ausführung der Testmethode als rote Fehlermeldungen im Quelltext an. Das Symbol links neben dem Methodenkopf gibt das Gesamttestergebnis für die Methode an. Es ist entweder ein grüner Punkt bei fehlerfreier Ausführung der Tests oder wie in Abbildung 6.15 ein roter Punkt mit einem weißen Kreuz. Sie können diese grünen und roten Symbole auch anklicken, um die entsprechende Methode und somit die darin enthaltenen Tests auszuführen.

Abbildung

Abbildung 6.15 Fehler bei der Ausführung eines Unit-Tests

Die Testmethode testCreation ist sehr sinnvoll. Sie können jetzt immer davon ausgehen, dass ein neues Puzzle sich in der Ausgangsposition befindet. Das ist eine gute Basis für weitere Testmethoden.

Das geht Sie nichts an

Ihre Testmethoden sollten die Testobjekte immer wie eine Blackbox behandeln. Sie dürfen also nicht auf die Interna der zu testenden Klassen zugreifen, sondern nur auf die öffentlichen Methoden und Propertys der Klassen. Dadurch können Sie die Implementierung der Klassen jederzeit ändern, ohne den Testcode anpassen zu müssen.

Abbildung 6.1 enthält ein Beispiel für die Veränderung des Puzzlemodells durch eine Zugfolge aus der Ausgangsstellung. Ein Puzzle in Ausgangsstellung, das Sie nach rechts, unten, rechts und wieder nach unten kippen, hat ein Array mit der Anordnung: [0, 1, 2, 3, 4, 15, 6, 7, 8, 5, 9, 11, 12, 13, 10, 14]. Das lässt sich doch wunderbar in einer Testmethode umsetzen, wenn Sie die Werte der Felder nach der Zugfolge in einem Array ablegen.

- (void)testComplexMove {
static NSUInteger theValues[] = {
0, 1, 2, 3, 4, 15, 6, 7, 8, 5, 9, 11, 12, 13, 10, 14
};

self.puzzle = [Puzzle puzzleWithLength:4];
XCTAssertTrue([self.puzzle tiltToDirection:PuzzleDirectionRight],
@"Can't tilt right.");
XCTAssertTrue([self.puzzle tiltToDirection:PuzzleDirectionDown],
@"Can't tilt down.");
XCTAssertTrue([self.puzzle tiltToDirection:PuzzleDirectionRight],
@"Can't tilt right.");
XCTAssertTrue([self.puzzle tiltToDirection:PuzzleDirectionDown],
@"Can't tilt down.");
XCTAssertTrue(self.puzzle.freeIndex == 5,
@"invalid free index: %u", self.puzzle.freeIndex);
for(NSUInteger i = 0; i < self.puzzle.size; ++i) {
NSUInteger theValue = [self.puzzle valueAtIndex:i];

XCTAssertTrue(theValue == theValues[i],
@"invalid value %d (%d) at index %d",
theValue, theValues[i], i);
}
}

Listing 6.19 Testmethode zu Abbildung 6.1

Die Testmethode in Listing 6.19 führt zunächst die vier Kippzüge aus. Dabei prüft sie auch, ob das Modell jeden Zug erfolgreich ausgeführt hat. Danach vergleicht die Methode die Anordnung der Teile im Modell (Ist-Wert) mit den Soll-Werten des Arrays theValues.

Aus Fehlern lernen

Ihr Programm hat einen Fehler? Wenn Sie die Fehlersituation in einem Testfall nachbilden, haben Sie eine gute Möglichkeit, den Fehler einfacher und schneller zu analysieren. Sie können ihn mit der Testmethode sozusagen unter Laborbedingungen untersuchen. Die Testbedingungen müssen natürlich das richtige Verhalten des Programms prüfen. Außerdem können Sie mit Hilfe der Testmethode prüfen, ob Sie den Fehler aus Ihrem Code eliminiert haben. Denn Ihre Testmethode zeigt so lange Fehler an, bis Ihr Code richtig funktioniert.

Die testgetriebene Softwareentwicklung geht sogar noch einen Schritt weiter. Bei diesem Vorgehen erstellt der Programmierer immer zuerst die Tests, bevor er den eigentlichen Programmcode erstellt. Er verändert dabei den Programmcode so lange, bis er alle Tests erfüllt.

Die beschriebenen Tests gehen davon aus, dass der Nutzer nur gültige Züge macht. Das sieht in der Praxis allerdings meistens anders aus. Auch diesen Fall sollten Sie in den Testfällen berücksichtigen. Beispielsweise darf das Kippen eines Puzzles in der Ausgangsstellung nach links das Puzzle nicht verändern. In diesem Fall muss die Methode tiltToDirection: den Wert NO liefern.

- (void)testInvalidMoves {
self.puzzle = [Puzzle puzzleWithLength:4];
XCTAssertFalse([self.puzzle
tiltToDirection:PuzzleDirectionLeft], @"tilt left.");
XCTAssertNil(self.notification, @"notification sent");
XCTAssertTrue(self.puzzle.solved, @"puzzle not solved");
XCTAssertFalse([self.puzzle
tiltToDirection:PuzzleDirectionUp], @"tilt up.");
XCTAssertNil(self.notification, @"notification sent");
XCTAssertTrue(self.puzzle.solved, @"puzzle not solved");
}

Listing 6.20 Testen unerlaubter Züge

Für nicht ausgeführte Züge darf das Modell natürlich auch keine Benachrichtigungen versenden. Um die Benachrichtigungen überprüfen zu können, registriert sich die Testklasse beim Notificationcenter und speichert die gesendeten Benachrichtigungen in der Property notification. Mit dem Makro XCTAssertNil(Ausdruck, Fehlermeldung, ...) können Sie überprüfen, ob ein Ausdruck nil ist. Die Testmethode verwendet es, um festzustellen, ob das Puzzle tatsächlich keine Benachrichtigung versendet hat.

- (void)setUp {
[super setUp];
[[NSNotificationCenter defaultCenter]
addObserver:self selector:@selector(puzzleDidTilt:)
name:kPuzzleDidTiltNotification object:nil];
self.puzzle = [Puzzle puzzleWithLength:4];
self.notification = nil;
}

- (void)tearDown {
[[NSNotificationCenter defaultCenter]
removeObserver:self];
self.notification = nil;
self.puzzle = nil;
[super tearDown];
}

- (void)puzzleDidTilt:(NSNotification *)inNotification {

self.notification = inNotification;
}

Listing 6.21 Registrierung für Benachrichtigungen in der Testklasse

Natürlich sollten die Tests auch den erfolgreichen Versand überprüfen. Da die Überprüfung einer Benachrichtigung mehrere Tests an verschiedenen Stellen umfasst, enthält die Testklasse dafür die Methode checkNotificationWithName:fromIndex:toIndex:, die verschiedene Aspekte der Benachrichtigung testet (siehe Listing 6.22).

- (void)checkNotificationWithName:(NSString *)inName 
fromIndex:(NSUInteger)inFromIndex
toIndex:(NSUInteger)inToIndex {
NSDictionary *theUserInfo = self.notification.userInfo;
NSUInteger theFromIndex = [[theUserInfo
valueForKey:kPuzzleFromIndexKey] unsignedIntValue];
NSUInteger theToIndex = [[theUserInfo
valueForKey:kPuzzleToIndexKey] unsignedIntValue];

XCTAssertNotNil(self.notification,
@"notification is nil");
XCTAssertNotNil(theUserInfo, @"userInfo is nil");
XCTAssertTrue(self.puzzle == self.notification.object,
@"invalid puzzle");
XCTAssertEqual(inName, self.notification.name,
@"invalid name %@ != %@", inName,
self.notification.name);
XCTAssertTrue(inFromIndex == theFromIndex,
@"invalid from index: %u != %u", inFromIndex,
theFromIndex);
XCTAssertTrue(inToIndex == theToIndex,
@"invalid from index: %u != %u", inToIndex,
theToIndex);
self.notification = nil;
}

Listing 6.22 Auswertung der Benachrichtigungen des Puzzles

Damit können Sie die Testmethode testComplexMove entsprechend erweitern:

XCTAssertTrue([self.puzzle 
tiltToDirection:PuzzleDirectionRight],
@"Can't tilt right.");
[self
checkNotificationWithName:kPuzzleDidTiltNotification
fromIndex:14 toIndex:15];
XCTAssertTrue([self.puzzle
tiltToDirection:PuzzleDirectionDown],
@"Can't tilt down.");
[self
checkNotificationWithName:kPuzzleDidTiltNotification
fromIndex:10 toIndex:14];
XCTAssertTrue([self.puzzle
tiltToDirection:PuzzleDirectionRight],
@"Can't tilt right.");
[self
checkNotificationWithName:kPuzzleDidTiltNotification
fromIndex:9 toIndex:10];
XCTAssertTrue([self.puzzle
tiltToDirection:PuzzleDirectionDown],
@"Can't tilt down.");
[self
checkNotificationWithName:kPuzzleDidTiltNotification
fromIndex:5 toIndex:9];

Listing 6.23 Überprüfung der Benachrichtigungen

Weitere Testklassen

Natürlich können Sie das Test-Target auch um weitere Testklassen erweitern. Dazu verwenden Sie den Menüpunkt New File... und die Vorlage Objective-C test case class. Häufig implementiert man die Testfälle einer Klasse in jeweils einer eigenen Testklasse.



Ihre Meinung

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.

<< zurück




Copyright © Rheinwerk Verlag GmbH, Bonn 2014
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.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern


  Zum Katalog
Zum Katalog: Apps programmieren für iPhone und iPad






Neuauflage: Apps programmieren für iPhone und iPad
Jetzt Buch 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: Spieleprogrammierung mit Android Studio






Spieleprogrammierung mit Android Studio


Zum Katalog: Android 5






Android 5


Zum Katalog: iPhone und iPad-Apps entwickeln






iPhone und iPad-Apps entwickeln


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
InfoInfo