Apps programmieren für iPhone und iPad – Das umfassende

Transcription

Apps programmieren für iPhone und iPad – Das umfassende
Kapitel 1
Hello iPhone
»Am Anfang wurde das Universum erschaffen. Das machte viele Leute
sehr wütend und wurde allenthalben als Schritt in die falsche Richtung
angesehen.«
– Douglas Adams
Was gibt es Schöneres als Erfolg? Das gilt im Fußball ebenso wie beim Lernen. Mit Hilfe dieses
Buches lernen Sie, Apps für iOS zu programmieren. Der Weg dahin ist nicht immer geradlinig, manchmal steinig; es gibt Tage, da hat man das Gefühl, nur auf der Stelle zu treten, und
an anderen geht es in rasendem Tempo bergauf, dem Ziel entgegen. Auf jeden Fall braucht es
etwas Geduld, bis man sich bis zu den Feinheiten durchgearbeitet hat und mehr als eine, aus
Standardkomponenten bestehende App programmieren kann.
Damit Ihnen der Weg lohnenswert erscheint und Sie nicht direkt zu Beginn von Theorie
erschlagen werden, beginnen wir dieses Buch mit der Programmierung einer kleinen App.
Die dabei auftretenden Begriffe erläutern wir in diesem Kapitel nur kurz, da es hier um die
Praxis geht; dieses Kapitel zeigt Ihnen, wie Sie die für die Programmierung notwendige
Arbeitsumgebung einrichten, Sie damit eine kleine App erstellen und diese anschließend auf
einem iPhone oder iPad zur Ausführung gelangt.
iPhone, iPad und iPod touch
Das iPhone besitzt einen kleinen Bruder, den iPod touch. Dieser iPod sieht dem iPhone sehr
ähnlich, ist aber flacher und verfügt über keine Mobilfunk-Funktionalität. Das Betriebssystem
des iPod touch ist dasselbe wie das des iPhones, also iOS. Daher kann ein für das iPhone
geschriebenes Programm in der Regel auch auf einem iPod touch laufen. Bei der iOS-Programmierung gibt es fast keine Unterschiede mehr zwischen iPhone und iPod touch. Der Übersichtlichkeit halber ist hier aber nur vom iPhone die Rede, was – bis auf sehr wenige Ausnahmen –
auch den iPod touch einschließt.
Dasselbe gilt auch für das iPad. Stellen, an denen Besonderheiten bei der Programmierung für
das iPad zu beachten sind, sind entsprechend gekennzeichnet. Für den Rest des Textes gebrauchen wir »iPhone«, »iPod« und »iPad« synonym.
23
1
Hello iPhone
1.1 Voraussetzungen
Um Apps für iOS programmieren zu können, benötigen Sie zwingend zwei Dinge:
왘 einen Apple-Computer mit Intel-Prozessor und einer möglichst aktuellen Version von
OS X, also Mavericks (10.9) oder Yosemite (10.10), und
왘 das Software Development Kit (SDK) für iOS.
Das SDK enthält alle notwendigen Programme und Komponenten für die App-Entwicklung;
die aktuelle Version hat die Nummer 6 und läuft nur unter Mavericks und Yosemite. Obwohl
Apple ältere Versionen von OS X noch länger pflegt, laufen aktuelle Versionen der SDKs und
anderer Programme von Apple häufig nur jeweils auf der aktuellsten Version. Der Druck ist
sanft aber bestimmt: für die neueste Version von Xcode braucht man die neueste Version
von OS X.
1.1
Voraussetzungen
1.1.1 Das SDK und die Entwicklerprogramme
Das SDK enthält neben einer integrierten Entwicklungsumgebung (IDE1) namens Xcode zahlreiche grafische Tools, mit denen die Entwicklung von iOS-Applikationen leicht von der
Hand geht, sowie die notwendigen Bibliotheken. Mit dem SDK können Sie Apps für iOS und
Programme für OS X erstellen. Sie können allerdings aus dem Stand heraus keine Apps auf
einem Endgerät testen. Dafür müssen Sie erst Mitglied im Apple Developer Network werden
und für das iOS Developer Program bezahlt haben.
Für zurzeit 80 EUR pro Jahr bekommen Sie dort2 ein Zertifikat, mit dem Sie eigene Applikationen signieren und diese auf bis zu 100 iOS-Geräte aufspielen können. Die Teilnahme am
iOS Developer Program ist überdies die Voraussetzung dafür, Apps in den iTunes Store3 einstellen zu können.
Das iOS Developer Program existiert in zwei Ausprägungen: Individual und Company. Die
Company-Version unterscheidet sich von der Individual-Version durch die Fähigkeit, Entwicklerteams zu verwalten, bietet dem einzelnen Programmierer jedoch keine weiteren Vorteile.
Die Mitgliedschaft im iOS Enterprise Program, die 279 € jährlich kostet, ermöglicht die
Installation selbstgeschriebener Apps auf mehr als 100 eigenen Geräten. Mit diesem Programm können Sie wiederum keine Apps in den Apple App Store hochladen. Das iOS Enterprise Program ist nur für Firmen gedacht, die hausinterne Apps entwickeln möchten. Für
private oder selbständige Entwickler sowie kleine Firmen ist dieses Programm daher in der
Regel eher uninteressant. Sowohl für das Enterprise Programm als auch für das Company
Developer Programm ist eine D-U-N-S-Nummer4 erforderlich.
Nutzer aus dem akademischen Bereich können am kostenlosen iOS University Program teilnehmen, das zwar den Test von selbstgeschriebenen Apps auf Endgeräten erlaubt, aber keine
Veröffentlichung im Apple App Store.
Hilfe von Apple inklusive
Bei allen kostenpflichtigen Programmen sind zwei Supportanfragen pro Jahr beim technischen Entwicklersupport von Apple enthalten. Sie können sich damit von Apple also bei der
Entwicklung Ihrer Programme individuell unterstützen lassen, wenn Sie auf Probleme stoßen.
Weitere Supportanfragen können Sie als Mitglied im Entwicklerprogramm kostenpflichtig
erwerben. Außerdem haben Sie Zugriff auf die Entwicklerforen, in denen sich auch die Entwickler von Apple tummeln.
Abbildung 1.1 Xcode zum Download im Mac App Store
Sie können das SDK als Bestandteil des Programms Xcode kostenlos über das Programm App
Store auf Ihren Mac herunterladen. Besondere Anforderungen an den Mac gibt es nicht.
Wenn Yosemite drauf läuft, ist er ohnehin so gut motorisiert, dass Xcode kein Problem
machen wird.
24
1
2
3
4
Integrated Development Environment
https://developer.apple.com/devcenter/ios
Häufig auch kurz als »App Store« bezeichnet.
http://www.dnb.ch/htm/690/de/Eindeutige-Identifikation.htm
25
1
Hello iPhone
1.1
Voraussetzungen
Falls Sie nicht die aktuelle Version des SDK verwenden möchten oder können, stehen Ihnen
nach der Anmeldung im iOS Developer Center unter der URL https://developer.apple.com/
downloads/index.action auch ältere SDK-Versionen zum Download zur Verfügung. Benutzer, die noch nicht mit Yosemite arbeiten, können dort auch noch eine Version für Mavericks oder Mountain Lion laden. An dieser Stelle aber die Warnung: Apple schneidet
bekanntermaßen gern, häufig und unvermittelt alte Zöpfe ab. Verlassen Sie sich also nicht
darauf, dass Apple Yosemite auch in Zukunft mit neuen SDKs versorgt. Aktualisieren Sie im
Zweifelsfall also lieber auf OS X 10.10, sofern das möglich ist; damit haben Sie immer Zugriff
auf die neueste SDK-Version.
Voraussetzungen für dieses Buch
Um die in diesem Buch gezeigten Beispiele nachzuprogrammieren, reicht das SDK ohne die
kostenpflichtige Registrierung als Entwickler aus. Alle Beispiele laufen – zumindest mit Einschränkungen – im Simulator.
Eine detaillierte Beschreibung von Xcode finden Sie in Kapitel 10, »Jahrmarkt der Nützlichkeiten«. Eine so umfangreiche IDE will erkundet und an die eigene Arbeitsweise angepasst
sein. Für den Einstieg genügt es allerdings vollkommen, wenn Sie den Anweisungen folgen,
die bei den Beispielen in diesem Buch stehen. Sie erklären alle Schritte ausführlich, verständlich und reproduzierbar, ohne dass Sie sich zunächst tiefer in Xcode einarbeiten müssen.
1.1.2 Der Simulator
Das SDK bringt einen iPhone- und iPad-Simulator mit. Dieser simuliert – wie der Name bereits vermuten lässt – das betreffende Endgerät. Dabei greift er auf die Ressourcen des HostComputers, also Ihres Mac-Rechners, zurück.
Mit dem iOS-Simulator können Sie die meisten Funktionen eines iOS-Gerätes nachstellen.
Die Geo-Lokalisierung funktioniert nur auf Rechnern mit Airport; allerdings lässt sich im
Simulator auch ein künstlicher Aufenthaltsort festlegen. Die Kamera, der Bewegungssensor
und Mobilfunk funktionieren im Simulator jedoch nicht. Hierfür müssen Sie eine App
immer erst auf einem durch das iOS Developer Program entsprechend freigeschalteten
Gerät installieren.
Das Dateisystem des Simulators, das er über das Dateisystem Ihres Macs abbildet, verhält
sich etwas anders als das eines echten iOS-Gerätes, und auch zentrale Ressourcen wie Adressbuch, Kalender und Keychain sind im Detail unterschiedlich. Der Simulator ist daher gut
geeignet, um Grundfunktionalitäten zu prüfen – Testläufe auf echten Geräten ersetzt er hingegen nicht.
Abbildung 1.2 Der iPhone-Simulator
Auch wenn Ihre App im Simulator perfekt laufen sollte, kommen Sie niemals um Tests auf
mindestens einem Gerät herum, wenn Sie Ihre App für andere Benutzer veröffentlichen
möchten. Hier ist eine – wahrscheinlich unvollständige – Liste mit Gründen:
왘 Ein iOS-Gerät hat eine andere Architektur als der Simulator beziehungsweise der Mac. Das
fängt schon mit unterschiedlichen Prozessorfamilien an, deren Befehlssätze vollkommen
unterschiedlich sind.
왘 Die Geschwindigkeit Ihrer App kann auf beiden Systemen stark variieren. Ein Programm,
das im Simulator flüssig läuft, kann auf einem iPod quälend langsam dahinkriechen.
왘 Ihr Mac hat vermutlich wesentlich mehr Hauptspeicher zur Verfügung als die 1 GB eines
aktuellen iPhones oder iPads. Während die App im Simulator stabil läuft, gibt sie auf dem
Gerät möglicherweise am laufenden Band Speicherwarnungen aus.
왘 Die Dateisysteme sind verschieden aufgebaut, und Sie können viele Verzeichnisse unter
iOS nicht lesen, geschweige denn schreiben. Das betrifft besonders das Ressourcenverzeichnis einer App, in das Sie auf dem iPhone nicht schreiben können, im Simulator hingegen schon.
왘 Der Simulator unterstützt nicht alle Hardware-Eigenschaften. Sie können keine Fotos mit
der Kamera schießen, und es gibt keinen Beschleunigungs- oder Gyrosensor.
26
27
1
Hello iPhone
Tipp
Bevor Sie Ihre App in den App Store hochladen, sollten Sie sie ausführlich auf echten Geräten
testen – am besten auf jedem Gerätetyp, den Ihre App unterstützt. Hierbei erweisen sich
gerade ältere Modelle als besonders wertvolle Testgeräte. Sie sind in der Regel langsamer und
haben weniger Arbeitsspeicher, so dass auf diesen Geräten entsprechende Programmfehler
wesentlich häufiger auftreten.
Wenn Ihre App kompatibel mit älteren iOS-Versionen sein soll, sollten Sie sie auf einem Gerät
mit jeweils diesen Betriebssystemversionen testen. Dadurch finden Sie inkompatiblen Code
am zuverlässigsten. Sie können grundsätzlich so gut wie alle alten iOS-Versionen unterstützen, das ergibt aber wirtschaftlich wenig Sinn. Stand Oktober 2014 waren nur noch 6 % aller
iOS-Geräte weltweit mit einer iOS-Version ausgestattet, die kleiner als 7.0 ist. Es empfiehlt sich
daher, mindestens iOS 7.0 als Deployment-Target festzulegen und in der Programmierung all
die vielen Annehmlichkeiten zu nutzen, die iOS 7 mitgebracht hat. Überdies bietet Xcode 6
ohnehin nur noch die Installation des Simulators für iOS 7 an und keine älteren. Sie können
zwar »zu Fuß« noch ältere Simulatoren installieren, aber das ergibt häufig wenig Sinn.
1.1
Voraussetzungen
Beim ersten Start von Xcode weist das Programm Sie gegebenenfalls an, weitere Bestandteile
zu installieren. Bestätigen Sie die Aufforderung einfach mit Install; eine Wahl haben Sie
ohnehin nicht.
Xcode unterwegs
Da Sie neben der Xcode-Installation aus dem App Store auch noch gegebenenfalls Zusatzpakete (Simulatoren) nebst Offline-Dokumentation nachladen müssen, sollten Sie vor der Verwendung von Xcode auf Reisen oder an Orten ohne Internetzugang vorher gut prüfen, ob
wirklich alle für Ihre Arbeit notwendigen Komponenten bereits installiert sind. Es ist zu
ärgerlich, im Flieger freudestrahlend das MacBook aufzuklappen, um mit Xcode zu arbeiten,
nur um dann festzustellen, dass Sie dafür noch einige hundert Megabyte an Daten installieren müssen.
1.1.3 Test der Arbeitsumgebung
Xcode hat eine eigene Versionsnummerierung, deren aktuelle Version 6 ist. Seit Version 4.3
befindet sich die Entwicklungsumgebung mit allen Komponenten im Programme-Ordner,
wohin der App Store auch alle anderen Applikationen installiert. Ziehen Sie das Xcode-Symbol am besten ins Dock, denn Sie werden das Programm im Laufe dieses Buches vermutlich
häufiger verwenden, und starten Sie es anschließend. Xcode begrüßt Sie mit folgendem
Startfenster:
Abbildung 1.4 Unterstützung für ältere iOS-Versionen
Abbildung 1.3 Das Startfenster von Xcode
28
Unter der Rubrik Documentation im selben Fenster können Sie die Entwicklerdokumentation herunterladen. Erstellen Sie, um die Lauffähigkeit Ihrer Entwicklungsumgebung zu
prüfen, über das Auswahlfeld Create a new Xcode project oder das Menü File 폷 New 폷
New Project… ein neues Projekt. Im nächsten Fenster wählen Sie in der linken Auswahlspalte in der Gruppe iOS den Punkt Application und rechts im Übersichtsfenster das Icon
Single View Application.
29
1
Hello iPhone
1.1
Voraussetzungen
Für den Company Identifier verwenden Sie in der Regel den vollqualifizierten Domainnamen in umgedrehter Schreibweise der Organisation, der das Projekt gehört. Beispielsweise gehört das Projekt in Abbildung 1.6 der Organisation Cocoaneheads mit der Domain
cocoaneheads.de. Der dazugehörende Company-Identifier ist somit »de.cocoaneheads«.
Aus Ihren Eingaben für die Felder Product Name und Company Identifier erzeugt Xcode
die App-ID und zeigt sie unter Bundle Identifier an. Sie stellt eine eindeutige Kennung für
die App dar, anhand derer ein iOS-Gerät die verschiedenen installierten Apps unterscheiden
kann. Dabei ist die Verwendung des Domainnamens als Basis für den Company-Identifier
eine einfache Möglichkeit, Namenskonflikte bei den Bundle-Identifiern verschiedener AppHersteller zu vermeiden.
Zusätzliche Einstellungen bei älteren Xcode-Versionen
Abbildung 1.5 Ein neues iOS-Projekt
Durch Auswahl des Buttons Next gelangen Sie zu dem in Abbildung 1.6 dargestellten Dialog.
Im nächsten Schritt geben Sie Ihrem Projekt einen Namen. Hier können Sie zwar einen beliebigen Text eingeben, es empfiehlt sich jedoch, Leerzeichen und Sonderzeichen zu vermeiden. Idealerweise besteht der Name nur aus Buchstaben und Ziffern.
In Xcode 4 können Sie in diesem Dialog zusätzlich den Speicherverwaltungstyp des Projekts,
die Benutzung von Storyboards und das Anlegen eines Test-Targets festlegen. Falls Sie noch
diese Xcode-Version verwenden, aktivieren Sie bitte die Optionen Automatic Reference
Counting und Use Storyboards, damit Ihr Projekt in diesen Punkten dem Beispielprojekt
entspricht.
Abbildung 1.6 Namen für das Projekt festlegen
Als Organization Name verwenden Sie den Namen der Organisation, Firma oder Person,
die das Urheberrecht an den Quellen des Projekts besitzt, da Xcode diesen Wert in die Copyright-Angaben der Quelltexte einfügt. Ansonsten findet der Organisationsname keine Verwendung.
30
Abbildung 1.7 Das Hauptfenster von Xcode
31
1
Hello iPhone
1.2
Im letzten Schritt bestimmen Sie den Ordner, in dem Xcode das Projekt ablegen soll, und
geben an, ob Xcode das Projekt unter eine eigene Git-Versionsverwaltung stellen soll.
Bestätigen Sie die Auswahl eines geeigneten Speicherortes, und beenden Sie damit den
Dialog. Es erscheint das Hauptfenster von Xcode mit den Projekteinstellungen (siehe Abbildung 1.7).
Wählen Sie nun oben links im Xcode-Fenster das Ausführungsziel für das soeben erstellte
Projekt aus – z. B. iPhone Retina (4-inch) –, und starten Sie anschließend mit dem Button
Run, der links neben dem Auswahlfeld sitzt, oder mit der Tastenkombination (cmd)+(R) die
Übersetzung des Projekts und die Ausführung im Simulator (siehe Abbildung 1.8).
Abbildung 1.8 Auswahl des Ausführungsziels
Xcode zeigt im Infobereich oben in der Mitte des Fensters den aktuellen Fortschritt an (siehe
Abbildung 1.9) und startet anschließend das Projekt im iPhone-Simulator.
Abbildung 1.10 Das Testprojekt funktioniert.
1.2 App geht’s
Abbildung 1.9 Fortschrittsanzeige von Xcode
Die Werkbank ist eingerichtet, jetzt ist es an der Zeit für das erste richtige Projekt. Damit
erhalten Sie einen Einblick in die Funktionsweise einer App. Sie sollen zunächst lernen, wie
Sie eine App in Xcode erstellen, mit der ein Benutzer interagieren kann und die darüber hinaus noch etwas mehr macht, als das obligatorische »Hallo Welt« auszugeben.
Wenn Sie im Simulator eine langweilige, weiße Fläche wie in Abbildung 1.10 sehen, hat alles
geklappt. Sie haben Xcode korrekt installiert, und das Übersetzen von Projekten funktioniert
auch. Damit ist es für Sie an der Zeit, die erste App zu programmieren.
Das folgende Beispielprojekt zeigt dem Benutzer einen Button an. Wenn er diesen drückt,
ruft die App im Hintergrund die Daten aus dem Internet zu einer URL ab und zeigt den Inhalt
der abgerufenen Seite an. Sie lernen dabei schon eine ganze Reihe wichtiger Grundbegriffe
kennen.
32
33
App geht’s
1
Hello iPhone
Projektinformation
Den Quellcode des folgenden Beispielprojekts finden Sie in den Materialien zum Buch
(www.galileo-press.de/3653) im Ordner Code/Apps/iOS7/HelloiPhone oder im Github-Repository zum Buch im Unterverzeichnis Apps/iOS7/HelloiPhone. Sie können das Github-Repository
über die URL https://github.com/cocoaneheads/iPhone/tree/Auflage_4 erreichen und dort
den kompletten Inhalt in einer Zip-Datei laden.
1.2
Wählen Sie im nächsten Schritt einen Speicherort für das Projekt aus, und öffnen Sie nach
dem Erscheinen des Xcode-Hauptbildschirms durch einen einfachen Klick die Datei
Main.storyboard5, die Sie im Projektnavigator auf der linken Seite finden. Dieses Storyboard
enthält die grafische Benutzerschnittstelle oder kurz GUI6 Ihrer App (siehe Abbildung 1.12).
Alternativ können Sie auch eine Arbeitskopie über die Anweisung git clone git://github.
com/cocoaneheads/iPhone.git erstellen. Falls Sie mit Git noch nicht vertraut sind, schlagen
Sie in Kapitel 10, »Jahrmarkt der Nützlichkeiten«, nach; dort ist die Arbeit mit Git ausführlich
erläutert. Außerdem zeigen wir Ihnen dort in einer detaillierten Anleitung, wie Sie das komplette Repository auschecken können.
1.2.1 Ein neues Projekt
Um die Beispiel-App zu erstellen, erzeugen Sie in Xcode ein neues Projekt vom selben Typ
wie im vorangegangenen Abschnitt (iOS 폷 Single View Application). Bei der Konfiguration
des Namens und der Projektparameter setzen Sie dieselben Angaben wie in Abbildung 1.11
gezeigt.
Abbildung 1.12 Die grafische Benutzerschnittstelle der neuen App
Da Sie beim Anlegen des Projekts eine App auf Basis eines einzelnen Views gewählt haben,
hat Ihnen Xcode mit dem Storyboard bereits diesen View und einen Viewcontroller angelegt. Ein View ist ein Objekt, das etwas anzeigt, während ein Viewcontroller die Anzeige des
Views steuert und dessen Eingaben (z. B. Antippen, Texteingaben, Tastendrücke) auswertet.
Wählen Sie auf der großen Arbeitsfläche in der Mitte des Xcode-Fensters den View – das ist
das weiße Rechteck – mit der Maus aus. Xcode hebt den View etwas hervor und zeigt links
daneben den Viewcontroller mit einem gelben Icon und den View mit einem weißen Icon in
einer Hierarchie an. Die Hierarchie befindet sich wiederum in einer Storyboard-Szene (siehe
Abbildung 1.13). Ein Storyboard kann beliebig viele solcher Szenen enthalten.
Abbildung 1.11 Projekteinstellungen für die Beispiel-App
34
5 In Xcode 4 heißt die Datei MainStoryboard.storyboard.
6 Graphical User Interface
35
App geht’s
1
Hello iPhone
1.2
Abbildung 1.15 Die Objektbibliothek
Abbildung 1.13 Eine Storyboard-Szene mit einem View
Öffnen Sie nun auf der rechten Seite des Xcode-Fensters die Utilities-Ansicht, indem Sie in
der Titelzeile auf den in Abbildung 1.14 gezeigten Button klicken.
Abbildung 1.14 Öffnen des Utilities-Bereiches
Abbildung 1.16 Label mit Beschreibung in der Objektbibliothek
Unten rechts in der Utilities-Ansicht finden Sie die Objektbibliothek, indem Sie das Würfelsymbol auswählen (siehe Abbildung 1.15).
Ziehen Sie ein Label mit der Maus oben links in den View, so wie es Abbildung 1.17 zeigt. Im
Zweifelsfall lassen Sie sich von den automatisch erscheinenden gestrichelten Hilfslinien inspirieren. Wenn Sie mit der Position zufrieden sind, lassen Sie das Label los, um es in den
View einzufügen.
Geben Sie in das Suchfeld ganz unten den Text »label« ein. Die Objektbibliothek zeigt Ihnen
dann nur noch ein Objekt mit dem Namen Label an, und wenn Sie es anklicken, öffnet
Xcode links daneben die entsprechende Beschreibung (siehe Abbildung 1.16).
36
37
App geht’s
1
Hello iPhone
1.2
Danach setzen Sie die Textausrichtung des Labels im Attributinspektor auf zentriert (siehe
Abbildung 1.19). Sie können den Attributinspektor öffnen, indem Sie das Label im View auswählen und die Tastenkombination (alt)+(cmd)+(4) drücken.
Abbildung 1.19 Ausrichtung der Label-Beschriftung im Attributinspektor
Suchen Sie anschließend in der Objektbibliothek nach »Button«, und ziehen Sie das linke
Objekt mit dem Namen Button (siehe Abbildung 1.20) ebenfalls auf den View. Ein Button
zeigt eine Schaltfläche an, mit der die Nutzer eine Aktion in der App auslösen können.
Abbildung 1.17 Platzieren eines Labels auf dem View
Damit das Label auch ausreichend Platz für den anzuzeigenden Inhalt hat, fassen Sie mit der
Maus den mittleren Begrenzungspunkt an der rechten Seite des Labels und ziehen ihn nach
rechts, bis eine weitere Hilfslinie erscheint (siehe Abbildung 1.18).
Abbildung 1.20 Buttons in der Objektbibliothek
Abbildung 1.18 Größenänderung des Labels
38
Um dem Button eine Beschriftung zu geben, führen Sie einen Doppelklick in dessen
Beschriftung aus und geben dort den Text »Go!« ein. Nach Abschluss dieser Arbeiten verfügt
39
App geht’s
1
Hello iPhone
Ihr View nun über ein Label und einen Button. Durch das Einfügen dieser Elemente haben
Sie auch die Hierarchie in der Storyboard-Szene geändert. Auch dort finden Sie nun das Label
und den Button. Den fertigen View und die neue Hierarchie sehen Sie in Abbildung 1.21. Die
in dieser Abbildung gezeigten blauen Rahmen um das Label und den Button können Sie über
den Menüpunkt Editor 폷 Canvas 폷 Show Bounds Rectangles einschalten; sie zeigen
Ihnen jeweils die genaue Position und Größe der Elemente an.
1.2
Übersetzen und starten Sie die App im Simulator über den Run-Button von Xcode oder mit
(cmd)+(R), wie Sie das bereits zum Testen Ihrer Xcode-Installation gemacht haben. Sie sehen
anschließend den weißen View mit dem Label und dem Button (siehe Abbildung 1.22) im
Simulator. Das Drücken des Buttons verändert zwar dessen Farbe, ansonsten tut sich aber
nichts.
Abbildung 1.22 Die App im Simulator
Abbildung 1.21 Der View mit Label und Button
40
Überdies sind Label und Button an den rechten Rand geschoben. Was daran liegt, dass die
GUI im Interface-Builder für alle Gerätetypen voreingestellt ist. Sie müssen in Xcode noch
festlegen, wo die beiden Elemente platziert werden sollen. Dazu dient das Autolayout. Öffnen Sie dazu noch einmal den Interface-Builder. Halten Sie die (cmd)-Taste gedrückt und
wählen mit gedrückter Taste das Label und den Button aus. Lassen Sie die (cmd)-Taste los und
wählen unten in der Xcode-Fußzeile den Button Align aus. Wählen Sie Horizontal Center
in Container, belassen den Wert auf 0 und bestätigen Sie die Auswahl mit dem Button Add
2 Constraints.
41
App geht’s
1
Hello iPhone
1.2
der URL anzeigt. Dazu müssen Sie einige Zeilen Code programmieren und den Button und
das Label mit diesem Code verbinden.
Abbildung 1.24 Was nicht passt, wird passend gemacht …
1.2.2 Sehr verbunden
Abbildung 1.23 Setzen Sie zwei Constraints, um Label und Button mittig zu platzieren.
Übersetzen Sie die App anschließend mit (cmd)+(R) neu; die beiden Elemente befinden sich
nun mittig in der GUI.
Autolayout ist ein extrem mächtiges Feature, mit dem Sie unweigerlich in Kontakt kommen,
wenn Sie für mehr als nur ein einziges Gerät programmieren. Mehr dazu erfahren Sie in den
folgenden Kapiteln.
Es fehlt jetzt noch die eigentliche Funktionalität der App, dass der Benutzer durch das Drücken des Buttons das Laden von Daten über eine URL auslöst und das Label dann den Inhalt
42
Die Verbindung von Code-Editor und GUI-Editor in Xcode macht das Erstellen dieser Verbindungen extrem einfach. In älteren Xcode-Versionen war der GUI-Editor, der Interface-Builder, ein externes, separates Programm; mit Xcode 4 hat Apple alles zu einem Programm
zusammengefasst.
Beim Anlegen des Projekts hat Xcode bereits eine Klasse für den Viewcontroller erzeugt, die
sich in die Dateien ViewController.h und ViewController.m aufteilt. Dateien mit der Endung .h
heißen Header-Dateien und deklarieren verschiedene Programmierelemente. Im Gegensatz
dazu heißen Dateien mit der Endung .m Implementierungsdateien. Sie enthalten den eigentlichen Programmcode. Die Header-Datei des Viewcontrollers beschreibt also, was diese
Klasse kann, und die Implementierungsdatei beschreibt, wie sie es macht.
43
App geht’s
1
Hello iPhone
1.2
Der Viewcontroller beherbergt die Logik des Bespielprogramms. Dazu müssen Sie zwei Verbindungen zwischen der Benutzeroberfläche und der Klasse ViewController herstellen. Wählen Sie dazu die Datei Main.storyboard aus, und öffnen Sie anschließend über die
Tastenkombination (alt)+(cmd)+(¢) oder den betreffenden Button in der Titelleiste den
Hilfs- beziehungsweise Assistant-Editor. Der Hilfseditor zeigt zu dem Haupteditor einen weiteren Editor an, dessen Inhalt in der Regel in einer Beziehung zu dem Inhalt des Haupteditors steht; beispielsweise zeigt er zu dem ausgewählten Viewcontroller im Storyboard
den Quelltext der passenden Implementierungsdatei an.
Abbildung 1.25 Öffnen des Assistant-Editors
Der Hilfseditor besitzt am oberen Rand eine Sprungleiste (Jump Bar). Wenn Sie dort den Eintrag ViewController.m anklicken, erscheint ein Popup-Menü (Abbildung 1.26). Darin wählen Sie den Eintrag ViewController.h aus, so dass der Hilfseditor diese Datei anzeigt.
Abbildung 1.27 Haupt- und Hilfseditor
Abbildung 1.26 Die Sprungleiste im Hilfseditor
Sie sollten nun im Haupteditor das Storyboard der App und im Hilfseditor den Inhalt der
Datei ViewController.h sehen (siehe Abbildung 1.27). Sie können jetzt Verbindungen von Elementen aus dem Storyboard direkt in die Header- oder Implementierungsdatei ziehen und
so die View-Elemente und Programmcode verbinden. Im vorliegenden Beispiel wollen wir
das Label aus dem Code heraus ansprechen, um seinen Wert ändern zu können. Darüber hinaus soll das Drücken des Buttons eine Aktion auslösen.
Ziehen Sie daher bei gedrückter (ctrl)-Taste mit der Maus eine Verbindung vom Label in
den Hilfseditor, und lassen Sie die Maustaste dort unterhalb der Zeile los, die mit dem
Schlüsselwort @interface beginnt (siehe Abbildung 1.28).
44
Abbildung 1.28 Verbindung vom Label zur Deklaration erstellen
Nach dem Loslassen erscheint der in Abbildung 1.29 gezeigte Pop-up-Dialog, in dem Sie die
Art der anzulegenden Verbindung definieren. Eine Verbindung von einem View-Element zu
einer Variablen nennt sich Outlet. Behalten Sie die Voreinstellungen bis auf den Namen bei,
für den Sie im entsprechenden Feld den Text »label« eingeben. Klicken Sie anschließend auf
Connect, um die Verbindung erstellen zu lassen.
45
App geht’s
1
Hello iPhone
1.2
Lassen Sie die Maustaste unter der Deklaration der Property los, und stellen Sie in dem sich
öffnenden Pop-up-Dialog den Verbindungstyp unter Connection auf Action (siehe Abbildung 1.31). Als Namen verwenden Sie »go«, und anschließend bestätigen Sie die Einstellungen durch Drücken des Buttons Connect.
Abbildung 1.29 Parameter für die Verbindung
Xcode erzeugt anschließend in der Datei ViewController.h die folgende Zeile:
@property (weak, nonatomic) IBOutlet UILabel *label;
Sie deklariert eine Property mit dem Namen label. Eine Property speichert eine Eigenschaft;
in diesem Fall ist das ein Verweis auf das Label im View.
Beim Button ist die umgekehrte Kommunikationsrichtung erforderlich: Das Drücken des
Buttons soll eine Aktion im Viewcontroller ausführen. Ziehen Sie dafür ebenfalls bei
gedrückter (ctrl)-Taste eine Verbindung vom Button in den Hilfseditor unter die eben von
Xcode erzeugte Zeile mit dem Outlet für das Label (siehe Abbildung 1.30).
Abbildung 1.31 Erstellen einer Action-Verbindung
Xcode legt dann in der Datei ViewController.h automatisch die folgende Zeile an:
- (IBAction)go:(id)sender;
Sie deklariert eine neue Methode namens go: . Methoden enthalten den eigentlichen
Programmcode des Programms; sie beschreiben also, wie ein Programm etwas macht.
Außerdem ergänzt Xcode die Datei ViewController.m um eine neue, leere Methodenimplementierung für die Methode go:, die Sie am Ende dieser Datei finden. Eine Methodenimplementierung sieht ihrer Deklaration sehr ähnlich, allerdings besitzt sie einen
Implementierungsblock, den ein geschweiftes Klammerpaar begrenzt. In diesem Block
steht der Programmcode der Methode, der ihre Funktionalität beschreibt. Der Block ist
allerdings noch leer.
- (IBAction)go:(id)sender {
}
Wenn der Nutzer den Button in der App drückt, ruft die App diese Methode auf. Sie können
das jetzt schon relativ einfach ausprobieren, indem Sie die Methode um eine Log-Anweisung erweitern, so dass die Datei ViewController.m die folgende Implementierung für die
Methode go: enthält:
- (IBAction)go:(id)sender {
NSLog(@"[+] go:");
}
Listing 1.1 Die erste Implementierung einer Methode
Abbildung 1.30 Eine Verbindung vom Button zum Code
46
Durch den Aufruf der Funktion NSLog veranlassen Sie die App, die Zeichenkette [+] go: in die
Konsole zu schreiben.
47
App geht’s
1
Hello iPhone
1.2
Übersetzen und starten Sie die App mit dem Run-Button oder über (cmd)+(R). Sobald der
Simulator den View mit dem Label und dem Button anzeigt, betätigen Sie den Button in der
App. Xcode öffnet automatisch am unteren Rand den Debug-Bereich, und darin erscheint die
Zeichenkette mit einem davorstehenden Zeitstempel wie in Abbildung 1.32. Das Drücken des
Buttons führt also zum Aufruf der vorgesehenen Methode.
if(theError == nil) {
NSLog(@"[+] IP: %@", theIP);
}
else {
NSLog(@"[+] Error: %@", [theError localizedDescription]);
}
[self.label setText:theIP];
}
Listing 1.3 Abfrage einer Webseite
Abbildung 1.32 Die Log-Ausgabe im Debug-Bereich von Xcode
Anstatt der Log-Ausgabe können Sie jedoch auch einen Text in der App anzeigen; Sie haben
ja schließlich das Label mit dem zugehörigen Outlet angelegt. Fügen Sie dazu die in Listing 1.2
hervorgehobene Zeile in die Methodenimplementierung von go: ein.
Dieser Code fragt den Inhalt der Webseite www.rodewig.de/ip.php ab und sendet ihn an das
Label in der Benutzeroberfläche der App. Die Methode erzeugt dafür zunächst eine URL und
merkt sich dieses Objekt in der Variablen theURL. Danach holt sie sich über die Methode
stringWithContentsOfURL:encoding:error: den Inhalt der Webseite. Dabei kann natürlich ein
Fehler auftreten, weil beispielsweise die Webseite nicht verfügbar ist. Wenn ein Fehler auftritt, speichert die Methode diesen Wert in der Variablen theError.
- (IBAction)go:(id)sender {
NSLog(@"[+] go:");
[self.label setText:@"Button gedrückt"];
}
Listing 1.2 Die erste Implementierung einer Methode
Wenn Sie nun die App im Simulator starten und den Button Go! drücken, ändert sich der
Text des Labels von Label in Button gedrückt. Das bewirkt die neu eingefügte Zeile: Sie
aktualisiert über die Methode setText: den angezeigten Text des Labels.
1.2.3 Netzwerk und Ausgabe
Im nächsten Schritt erweitern Sie die Funktionalität der App um den Datenabruf. Dazu
ändern Sie in der Datei ViewController.m die Methode go: wie folgt:
- (IBAction)go:(id)sender {
NSLog(@"[+] go:");
NSError *theError = nil;
NSURL *theURL = [NSURL URLWithString: @"http://www.rodewig.de/ip.php"];
NSString *theIP = [NSString stringWithContentsOfURL:theURL
encoding:NSASCIIStringEncoding error:&theError];
Abbildung 1.33 Es klappt – die App zeigt die IP-Adresse an.
48
49
App geht’s
1
Hello iPhone
Nach der Abfrage erfolgt eine Fallunterscheidung: Entweder trat kein Fehler auf, dann zeigt
die Methode den Inhalt der Webseite im Log an. Andernfalls macht sie das mit der Fehlermeldung. Die Unterscheidung erfolgt dabei anhand der Variablen theError. Wenn diese keinen Fehler enthält, hat sie den Wert nil. Dieser Wert bedeutet, dass die Variable kein Objekt
enthält. Zum Schluss aktualisiert die Methode noch die Anzeige des Labels mit dem gelesenen Inhalt der Webseite, der sich in der Variablen theIP befindet.
Stoßen Sie jetzt erneut über (cmd)+(R) die Übersetzung und den Start der App an. Sofern Ihr
Rechner über eine Verbindung mit dem Internet verfügt, zeigt die App nach dem Drücken
des Buttons Ihre externe IP-Adresse an (siehe Abbildung 1.33).
1.2
Darüber hinaus gibt es Entitlements, mit denen Sie spezielle Berechtigungen für eine App
vergeben können. Den Zugriff auf die iCloud oder das Empfangen von Apple Push Notifications regeln beispielsweise Entitlements; dazu jedoch später mehr. Eine App ohne diese
besonderen Anforderungen benötigt auch keine speziellen Entitlements.
Sie können in Xcode zwar auch ohne gültiges Entwicklerzertifikat ein iOS-Gerät als Ziel auswählen, wie Abbildung 1.34 zeigt, das Ausführen des Projektes schlägt indes fehl (siehe Abbildung 1.35).
Gratulation zu Ihrer ersten App! Allerdings gibt es an ihr noch eine Kleinigkeit zu verbessern: Nach dem Start steht der Text des Labels auf »Label«. Das ist natürlich unschön. Um
dies zu ändern, fügen Sie in der Datei ViewController.m über der go:-Methode noch die folgende Methode hinzu:
- (void)viewWillAppear:(BOOL)inAnimated {
[super viewWillAppear:inAnimated];
NSLog(@"[+] viewWillAppear:");
[self.label setText:@"Moin"];
}
Listing 1.4 Setzen eines anderen Anfangstextes
Sie setzt den initialen Wert des Labels auf »Moin«. Wenn Sie die App jetzt erneut übersetzen
und ausführen, zeigt das Label nach dem Start der App diesen Text an.
Abbildung 1.34 Auswahl eines nicht näher spezifizierten iOS-Gerätes
1.2.4 Test auf einem Gerät
Wie wir in Abschnitt 1.1.1, »Das SDK und die Entwicklerprogramme«, erwähnt haben, benötigen Sie ein kostenpflichtiges Entwicklerzertifikat, um selbstprogrammierte Apps auf einem
iPhone installieren und ausführen zu können. iOS führt zum Schutz vor Schadsoftware (und
zur Wahrung von Apples Geschäftsmodell) ausschließlich Code aus, der mit einem von
Apple ausgestellten, gültigen Zertifikat signiert ist. Dieses Verhalten nennt sich Mandatory
Code Signing oder kurz Code Signing.
Das Code Signing besteht aus vier Elementen. Das erste Element sind Zertifikate. Jede App
muss ein von Apple ausgestelltes und signiertes Zertifikat besitzen, ansonsten verweigert
der iOS-Kernel das Laden der App. Damit ein Gerät eine selbstgeschriebene und mit dem
eigenen Entwicklerzertifikat signierte App als gültig akzeptiert, muss es ein passendes Provisioning Profile besitzen. Ein Provisioning Profile oder Bereitstellungsprofil verbindet das Entwicklerzertifikat mit einer App und einem Gerät, auf dem die App laufen soll.
50
Abbildung 1.35 Ohne Moos nix los.
1.2.5 Entwicklerzertifikat und Entwicklungsprofile
Um nach dem Abschluss einer kostenpflichtigen Mitgliedschaft als iOS-Entwickler ein Gerät
für die Entwicklung nutzen zu können, richten Sie in Xcode als erstes Ihren EntwicklerAccount ein. Dazu öffnen Sie die Einstellungen über Xcode 폷 Preferences 폷 Accounts und
fügen dort Ihren Account ein.
51
App geht’s
1
Hello iPhone
1.2
Sie können das angeschlossene Gerät nun umgehend in Xcode als Target auswählen.
Abbildung 1.36 Die Account-Einstellungen von Xcode
Anschließend schließen Sie Ihr Testgerät an den Mac an, öffnen Xcode und darin den Organizer über den Menüpunkt Window 폷 Devices. Im Tab Devices sehen Sie links iPhones und
iPads, die Sie mit Xcode bereits verwendet haben und auch Ihr gerade angeschlossenes
Gerät.
Abbildung 1.38 Das Gerät als Target für das aktuelle Projekt
Wenn Sie das frisch angeschlossene Gerät als Target auswählen und das Projekt starten, meldet Xcode, dass es keine gültige Identität für das Code Signing finden konnte. Wählen Sie die
Option Fix Issue.
Abbildung 1.39 Es fehlt noch das Entwicklerzertifikat.
Xcode fragt, ob es den bereits eingerichteten Account verwenden soll. Bei mehreren in
Xcode eingerichteten Accounts können Sie den gewünschten auswählen.
Abbildung 1.37 Die Geräteübersicht von Xcode
52
Abbildung 1.40 Der Entwickler-Account für den Test auf dem Gerät
53
App geht’s
1
Hello iPhone
Anschließend erzeugt Xcode ein Entwicklerzertifikat samt privatem Schlüssel, den Sie zum
Signieren der Apps benötigen, und hinterlegt beides im Schlüsselbund.
1.2
in den Ordner, in dem sich Ihr Zertifikat befindet, und geben Sie dort den folgenden Befehl
ein:
openssl x509 -text -inform der -in ios_development.cer
Das Ergebnis ist eine Darstellung der im Zertifikat gespeicherten Informationen wie in Abbildung 1.43.
Abbildung 1.41 Xcode erzeugt ein Zertifikat samt privatem Schlüssel.
Nach Abschluss des Vorgangs führt Xcode das Projekt dann problemlos auf dem Gerät aus,
solange Sie das Gerät nicht mit einem Code gesperrt haben; zum Testen müssen Sie das
Gerät stets entsperren.
Abbildung 1.42 Xcode umgeht nicht die Code-Sperre. Und das ist auch gut so.
Das Entwicklerzertifikat sowie die Bereitstellungsprofile erhalten Sie nicht nur über Xcode,
sondern auch im iOS Provisioning Portal. Dieses finden Sie online, wenn Sie im iOS Dev Center7 rechts oben im Kasten iOS Developer Program auf das Element iOS Provisioning
Portal klicken.
Vorsicht mit dem Schlüsselbund!
Ein Zertifikat besteht aus einem privaten Schlüssel, den Sie benötigen, um damit Operationen
(wie z. B. das Signieren) durchzuführen. Das Zertifikat selbst ist der öffentliche Schlüssel, über
den Dritte – in diesem Fall der iOS-Kernel Ihres Entwicklungsgeräts – Ihre Identität prüfen können. Den privaten Schlüssel zum Zertifikat legt Xcode bei der Erzeugung des Zertifikates im
Schlüsselbund Ihres Rechners ab. Apple kennt diesen Schlüssel nicht und hat auch keine
Sicherheitskopie. Das bedeutet, dass Sie gut auf Ihren Schlüsselbund und die darin enthaltenen Objekte achten müssen. Denn wenn Ihnen der private Schlüssel abhandenkommt, können Sie Ihr Zertifikat nicht mehr verwenden und müssen ein neues ausstellen.
Wenn Sie das Zertifikat auf Ihren Mac herunterladen, können Sie sich mit dem bordeigenen
OpenSSL-Paket die Details anschauen. Öffnen Sie dazu das Programm Terminal, wechseln Sie
7 https://developer.apple.com/devcenter/ios/index.action
54
Abbildung 1.43 Das Entwicklerzertifikat in der Detailansicht
1.2.6 Apps mit speziellen Funktionalitäten
Jede App benötigt zur Ausführung eine App-ID. Das ist eine Kennung, für die die Berechtigungen einer App bei Apple hinterlegt sind. Vor Xcode 5 erfolgte die Konfiguration der AppIDs ausschließlich über das Entwicklerportal; analog zum Erstellen eines Entwicklerzertifikates können Sie dies nun bequem über Xcode erledigen.
Öffnen Sie dazu das Capabilities-Tab in den Projekteinstellungen. Dort finden Sie alle
Berechtigungen, die Sie einer App zuweisen können (siehe Abbildung 1.44).
55
App geht’s
1
Hello iPhone
1.3
Zurück auf Los: Swift
Wenn Sie den Button Choose drücken, legt Xcode gegebenenfalls im Entwicklerportal eine
passende App-ID mit den dazugehörenden Berechtigungen an. Nach Abschluss der Kommunikation zwischen Xcode und dem Entwicklerportal aktiviert Xcode die betreffende Berechtigung.
Abbildung 1.46 Aktivierung der Datenverschlüsselung
App-IDs are forever
Sie können im Entwicklerportal oder über Xcode beliebig viele App-IDs manuell anlegen. Nachdem Sie jedoch eine App-ID angelegt haben, lässt sie sich weder löschen noch verändern, und
selbst eine Supportanfrage bei Apple hat wenig Erfolgschancen. Noch nicht einmal die
Beschreibung können Sie anpassen. Sie sollten sich also vor dem Anlegen genau überlegen, ob
Sie wirklich diese Kennung benötigen und welche Werte Sie dafür eingeben. Am besten legen
Sie also eine neue App-ID im Portal erst an, wenn Sie sie auch tatsächlich benötigen.
1.3 Zurück auf Los: Swift
Abbildung 1.44 Mögliche Berechtigungen für eine App
Sie lernen im Laufe dieses Buches noch einige dieser Berechtigungen kennen. An dieser
Stelle führen wir Ihnen vor, wie Sie einer App diese Berechtigungen hinzufügen. Dazu aktivieren Sie die betreffende Berechtigung über den korrespondierenden On/Off-Schalter auf
der rechten Seite. Xcode fragt Sie anschließend nach dem für diesen Vorgang zu verwendenden Entwicklerkonto (siehe Abbildung 1.45).
Abbildung 1.45 Das Entwicklerkonto für die Berechtigungsoperation
56
Apple wäre nicht Apple, wenn es nicht alle Nase lang Neuerungen in der Produktpalette gäbe.
Dies bezieht sich nicht nur auf neue Geräte und Software, sondern auch auf Neuerungen, die
den Programmierer betreffen. An das jährliche Major-Update von iOS und OS X und die
damit einhergehende jährlich neue Xcode-Version konnte man sich als Entwickler nun seit
2010 gewöhnen. Doch Apple wäre eben auch nicht Apple, wenn ein vorgegebenes Muster
über einen längeren Zeitraum ausreichend wäre. Zusätzlich zu den ohnehin in diesem Jahr
mit iOS 8 erschienenen Neuerungen, hat Apple im Juni 2014 auf der WWDC eine neue Programmiersprache angekündigt, Swift. Vermutlich jeder halbwegs erfahrene Objective-C-Programmierer musste nach der Ankündigung von Swift durch Craig Federighi (»Hair Force
One«) auf der WWDC seinen Unterkiefer von der Tischplatte aufsammeln. Eine neue Programmiersprache, zudem eine, die die alten Zöpfe von C abschneidet und gleichzeitig mit
vielen in Objective-C etablierten Paradigmen und Techniken bricht.
Das Echo auf Swift unter den Entwicklern ist nach wie vor geteilt. Möchte man sich auf die
Chancen kaprizieren, kann Swift das Zeug haben, die Zahl der Programmierer für iOS und
OS X signifikant zu vergrößern; ist doch der größte Nachteil an Objective-C die hohe Einstiegshürde durch die sprachspezifischen Besonderheiten. Hat man sich allerdings jahrelang
mühsam genau diese Besonderheiten angeeignet, reibt man sich nun verwundert die Augen
und findet in Swift Konstrukte wieder, über die man als Objective-C-Programmierer jahrelang nur müde gelächelt hat. Details zu Swift lernen Sie in Abschnitt 2.8, »Swift«, kennen.
57
1
Hello iPhone
Damit Sie einen Einblick in Swift erhalten, werden Sie im Folgenden die in Abschnitt 1.2,
»App geht’s«, in Objective-C programmierte App noch einmal bauen. Dieses Mal in Swift.
Starten Sie Xcode und legen ein neues iOS-Projekt vom Typ Single View Application an. In
dem darauf folgenden Dialog wählen Sie als Sprache allerdings nicht Objective-C, sondern
Swift aus.
1.3
Zurück auf Los: Swift
Von diesen Unterschieden abgesehen, gehen Sie im Folgenden genauso vor wie in der Objective-C-Version; öffnen Sie also die Datei Main.storyboard im Interface-Builder und ebenda
dann die Objektbibliothek. Ziehen Sie anschließend ein Label und einen Button auf den
View, genauso wie in Abschnitt 1.2.1, »Ein neues Projekt«, beschrieben. Das Label passen Sie
so an, dass es über die ganze View-Breite reicht.
Wenn Sie danach den Assistant-Editor öffnen, sehen Sie in der Datei ViewController.swift,
dass die Struktur zwar der aus der Objective-C-Version ähnelt, die Syntax aber eine ganz
andere ist. Herzlich willkommen in Apples neuer Programmierwelt!
Ziehen Sie vom Label ein Outlet »label« in die Datei ViewController.swift und vom Button
eine Action »go«. Der Inhalt der Datei ist anschließend wie folgt:
import UIKit
Abbildung 1.47 Swift ist diese Mal die Sprache der Wahl.
Nach Abschluss des Dialogs begrüßt Sie Xcode in derselben Ansicht wie beim ersten Durchlauf in Abschnitt 1.2.1, »Ein neues Projekt«. Wobei genau genommen zwei Änderungen festzustellen sind: Es gibt jeweils nur noch eine Datei AppDelegate und ViewController, und
beide Dateien haben die Endung .swift.
class ViewController: UIViewController {
@IBOutlet weak var label: UILabel!
@IBAction func go(sender: AnyObject) {
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
Listing 1.5 Die Datei »ViewController.swift« nach Anlegen des Outlets und der Action
Zunächst passen Sie die Datei ViewController.swift so an, dass die App nach dem Start dasselbe freundliche »Moin« zeigt wie die Objective-C-Variante. Dazu erweitern Sie die Methode
viewDidLoad() um die entsprechende eine Anweisung:
override func viewDidLoad() {
super.viewDidLoad()
label.text = "Moin"
}
Listing 1.6 Anzeige von »Moin« beim Start der App
Neben der ohnehin anderen Syntax gegenüber Objective-C fällt hier direkt auf, dass das
Schlüsselwort self für den Zugriff auf die Property label nicht mehr notwendig ist.
Abbildung 1.48 Der kleine, aber feine Unterschied
58
Um jetzt noch die Funktion einzubauen, mit der das Drücken des Buttons zum Abruf der IPAdresse führt, erweitern Sie die Action-Methode go um die folgenden Anweisungen:
59
1
Hello iPhone
@IBAction func go(sender: AnyObject) {
let theURL = NSURL.URLWithString("http://www.rodewig.de/ip.php")
var theIP = NSString.stringWithContentsOfURL(theURL, encoding:
NSUTF8StringEncoding, error: nil)
label.text = theIP
}
Listing 1.7 Die Action-Methode »go« in Swift
Führen Sie die App nun aus und drücken den Button.
1.4
Zusammenfassung
Wie geht es weiter mit Swift
Swift ist eine sehr junge Programmiersprache. Die Entwickler bei Apple, die Swift erfunden
und implementiert haben, arbeiten schon seit einigen Jahren an Swift. Öffentlich verfügbar ist
Swift allerdings erst seit der WWDC im Juni 2014. Im September 2014 wurde die Version 1.0
veröffentlicht.
Vieles an Swift ist noch nicht ganz ausgereift, was bei dem geringen Alter der Sprache nicht
verwundert, und insbesondere die iOS-Bibliothek ist noch komplett auf Objective-C ausgerichtet, wenngleich aber mit Swift nutzbar. Die Vehemenz und Geschwindigkeit, mit der Apple
Swift in den Markt drückt, deutet stark darauf hin, dass Swift keine Ergänzung, sondern der
Nachfolger von Objective-C werden wird. Auch wenn Chris Lattner, der Erfinder von Swift, im
Internet geäußert hat, dass es keinen Plan gibt, Objective-C durch Swift abzulösen, ist den
meisten langjährigen Programmierern für iOS und Mac klar, dass es genauso kommen wird.
Apple ist erwiesenermaßen unglaublich rigoros, wenn es um das Ersetzen »alter« Technologien geht.
Da die gesamte iOS-Bibliothek allerdings noch auf Objective-C angepasst ist und viele Konzepte in dieser Bibliothek sich gar nicht oder nur schlecht mit Swift vereinbaren lassen, wird
noch eine Menge Wasser den Rhein herunterfließen, bis Apple Objective-C komplett rausschmeißen kann. Daher lautet unser Rat: Auch am Ende des Jahres 2014 sind Sie gut damit
beraten, Objective-C zu lernen, wenn Sie die iPhone-Programmierung beherrschen möchten.
Neue Apps können Sie getrost in Swift beginnen. Wenn Sie aber bestehende Projekte oder Bibliotheken Dritter verwenden möchten, werden Sie auf lange Zeit um Objective-C nicht herumkommen.
1.4 Zusammenfassung
In diesem Kapitel haben Sie die ersten Schritte in der iOS-Programmierung getan. Sie haben
erfolgreich das iOS-SDK installiert und eine App programmiert, die über das übliche »Hallo
Welt« hinausgeht. Dabei haben wir detaillierte Erklärungen bewusst ausgespart, um Ihnen
diese Schritte möglichst zu vereinfachen und Ihnen so die Leichtigkeit der iOS-Programmierung zu zeigen. Mit den Grundlagen macht das nächste Kapitel Sie ausgiebig vertraut.
Sie sollten die folgenden Erkenntnisse aus dem vorangegangenen Beispiel behalten, denn
die Erklärungen dazu geben Ihnen die folgenden Kapitel:
왘 Eine Klasse – was das auch immer sein mag – ist in zwei Dateien aufgeteilt (.h und .m).
Abbildung 1.49 Die App funktioniert. Sieht aber nicht gut aus.
Label und Button auf der App-Oberfläche sind auch hier verrutscht. Um beide auszurichten,
verfahren Sie, wie in Abschnitt 1.2.1 beschrieben (»Autolayout«).
Dabei beschreibt die Header-Datei (.h), was eine Klasse kann, und die Implementierungsdatei (.m), wie sie es macht.
왘 Die Anweisungen, »etwas zu tun«, stehen in Methoden.
왘 Sie können Variablen und Methoden relativ einfach mit einem Element der Benutzer-
oberfläche verbinden.
60
61
1
Hello iPhone
왘 Über die Methode viewWillAppear: können Sie vor der Anzeige eines Views automatisch
Aktionen ausführen.
왘 Swift scheint zum Erfolg verdammt zu sein, braucht aber noch seine Zeit.
Neben diesen ersten Schritten zur Programmierung haben Sie außerdem gelernt, wie Sie
Ihre Apps auf einem Gerät ausführen und wo Sie dabei nach Fehlern suchen können, wenn
es nicht funktioniert. Ferner haben Sie erfahren, wozu Zertifikate und Profile dienen, und
dabei dem Provisioning Portal einen kleinen Besuch abgestattet.
62
Kapitel 6
Models, Layer, Animationen
»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 View-Wechsel 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 im Github-Repository zum
Buch im Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Ausgabe_iOS8/
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.
603
6
Models, Layer, Animationen
6.1
6.1 Modell und Controller
Modell und Controller
Konsistenz des Schiebepuzzles
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.
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.
6.1.1 iOS Next Topmodel
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
Um die Konsistenz des Puzzlemodells sicherzustellen, liegen ihm folgende Regeln zugrunde:
es auch die Konsistenz der Spieledaten sicherstellen, und das ist komplizierter als bei vielen
1. Ein neues Puzzle hat immer die Ausgangsstellung.
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
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.
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.
0
1
2
3
0
1
2
3
4
5
6
7
4
15
6
7
8
9
10
11
8
5
9
11
12
13
14
15
12
13
10
14
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].
604
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.
605
6
Models, Layer, Animationen
6.1
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;
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.
1
2
Index des Feldes für den Tausch
oben
freeIndex + 4
unten
freeIndex - 4
Tabelle 6.1 Regeln für das Kippen des Puzzles (Forts.)
Listing 6.2 Aufzählungstyp mit den möglichen Bewegungsrichtungen
0
Kipprichtung
3
Oben
Rechts
4
5
6
7
8
9
10
11
12
13
14
15
Links
Unten
Right
Abbildung 6.2 Spielzüge im Puzzle
Daraus können Sie, ausgehend vom freien Feld an der Position freeIndex, die Regeln für das
Kippen herleiten (siehe Tabelle 6.1).
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);
Kipprichtung
Index des Feldes für den Tausch
}
links
freeIndex + 1
rechts
freeIndex - 1
- (BOOL)columnOfIndexFreeIndexIsEqualColumnOfIndex: (NSUInteger)inToIndex {
NSUInteger theLength = self.length;
NSUInteger theSize = self.size;
NSUInteger theIndex = self.freeIndex;
Tabelle 6.1 Regeln für das Kippen des Puzzles
606
Modell und Controller
607
6
Models, Layer, Animationen
6.1
return inToIndex < theSize && (theIndex % theLength) == (inToIndex % theLength);
while(theIndex == self.freeIndex) {
theIndex = drand48() * theSize;
}
return theIndex;
}
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 –
Modell und Controller
}
Listing 6.4 Berechnung der nächsten Position für die Methode »shuffle«
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 des
angegebenen Feldes 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
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;
tiltToDirection: um eins.
if(inIndex == theFreeIndex) {
return PuzzleNoDirection;
}
else if([self rowOfFreeIndexIsEqualToRowOfIndex: inIndex]) {
return inIndex < theFreeIndex ? PuzzleDirectionRight : PuzzleDirectionLeft;
}
else {
return inIndex < theFreeIndex ? PuzzleDirectionDown : PuzzleDirectionUp;
}
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 drand48() 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 srand48() mit der aktuellen Uhrzeit über die Anweisung
}
Listing 6.5 Berechnung einer Kipprichtung zu einer Position
srand48(time(NULL));
aufruft, wobei die Funktion time() die aktuelle Systemzeit liefert. 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). Da die Rückgabewerte der Funktion drand48()
größer oder gleich 0 und kleiner als 1 sind, liegt der Ganzzahlwert von theIndex immer zwischen 0 und self.size - 1 einschließlich.
- (NSUInteger)nextIndex {
NSUInteger theSize = self.size;
NSUInteger theIndex = drand48() * theSize;
608
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) {
609
6
Models, Layer, Animationen
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
6.1.2 View an Controller
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;
6.1
Modell und Controller
[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 Feldes 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.
6.1.3 Gerätebewegungen auswerten
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-MotionFramework ü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).
[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 {
Abbildung 6.3 Hinzufügen des Core-Motion-Frameworks
610
611
6
Models, Layer, Animationen
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.
6.1
Modell und Controller
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).
+y
self.motionManager = [CMMotionManager new];
[self.motionManager setAccelerometerUpdateInterval:0.1];
-z
Listing 6.8 Erzeugung und Initialisierung des Beschleunigungssensors
Die Kategorie UIViewController(Games) stellt über die Methode motionManager den Motionmanager 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 Hauptqueue 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];
}
}];
}
+x
-x
+z
-y
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 = –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.
Listing 6.9 Starten der Beschleunigungssensor-Abfragen
Do it yourself
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 Beschleunigungssensor-Abfragen
612
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 Help 폷 Documentation
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
613
Models, Layer, Animationen
die Konstante, in deren Bereich sich der x- und der y-Wert befinden. 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.
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.
PuzzleDirectionUp
y
0,5
0,2
- (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 Beschleunigungssensor-Werte
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 PropertyWert lastDirection auf PuzzleNoDirection. Die Methode setzt den Property-Wert jeweils auf
Modell und Controller
PuzzleNoDirection
–0,5
–0,2
0,2
x
0,5
PuzzleDirectionRight
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.
6.1
PuzzleDirectionLeft
6
–0,5
PuzzleDirectionDown
Abbildung 6.5 Auswertungsbereiche für Beschleunigungswerte
6.1.4 Modell an Controller
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 riechen1.
1 Siehe http://de.wikipedia.org/wiki/Code_smells.
614
615
6
Models, Layer, Animationen
6.1
2. Sie können komplexere Veränderungen des Modells unter Umständen nur sehr schlecht
Modell und Controller
View
über dieses Vorgehen abbilden. Die Methode shuffle führt beispielsweise sehr viele Vertauschungsoperationen durch.
Eingabe
3. Wenn nicht nur ein, sondern mehrere Controller das Modell verändern können, können
Aktualisierung
der Modellinhalt und die View-Darstellung voneinander differieren.
Controller
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
?
Auswertung der
Eingabe
Auswertung der
Benachrichtigung
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
Modell
Aktualisierung
Versand bei einer
Benachrichtigung
Benachrichtigungen und aktualisieren sich und den View entsprechend.
Das Modell des Schiebepuzzles versendet zwei Benachrichtigungen mit jeweils gleich aufge-
Abbildung 6.6 Aktualisierung des Views über Modellaktualisierungen
bautem Info-Dictionary. Die Methode tiltToDirection: verschickt die Benachrichtigung
kPuzzleDidTiltNotification, während moveItemAtIndex:toDirection: die Benachrichtigung
kPuzzleDidMoveNotification versendet. Das Directory userInfo in der Benachrichtigung ent-
hält dabei die folgenden Schlüssel:
Schlüssel
Wert
kPuzzleDirectionKey
Die Bewegungsrichtung des Puzzleteils
kPuzzleFromIndexKey
Der ursprüngliche Index des Puzzleteils
kPuzzleToIndexKey
Der neue Index des Puzzleteils
Tabelle 6.2 Schlüssel des User-Info-Dictionarys
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:
Die Werte in der Tabelle haben alle den Typ NSUInteger. Die muss sie jedoch in NSNumber-
[self.puzzle addObserver:self forKeyPath:@"moveCount" options:0 context:nil];
Objekte kapseln, um sie in einem NSDictionary verwenden zu können.
Listing 6.12 Registrierung als Beobachter eines Wertes
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.
616
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.
617
6
Models, Layer, Animationen
6.1.5 Undo und Redo
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
6.1
Modell und Controller
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 RedoOperationen dar. Die durchgezogenen Pfeile sind mit der aufgerufenen Methode des UndoManagers beschriftet und zeigen die Herkunft des ausgeführten Methodenaufrufs an.
Undo-Stack
Redo-Stack
title = @"Alt"
setTitle:@"Neu"
setTitle:@"Alt"
clear
Undo-Stack
title = @"Neu"
Redo-Stack
setTitle:@"Alt"
undo
setTitle:@"Neu"
setTitle:@"Alt"
Undo-Stack
Redo-Stack
title = @"Alt"
setTitle:@"Neu"
redo
setTitle:@"Neu"
setTitle:@"Alt"
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.
Undo-Stack
title = @"Neu"
Redo-Stack
setTitle:@"Alt"
Abbildung 6.7 Interaktion des Setters mit dem Undo-Manager
618
619
6
Models, Layer, Animationen
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];
6.1
Modell und Controller
self.preparedTarget = nil;
// inoutInvoction auf Undo- oder Redo-Stack legen
}
Listing 6.16 Invocation-Erzeugung über Proxyaufruf
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.2 Das ist das gleiche Prinzip wie bei dem Entwurfsmuster.
Mit der Methode tiltToDirection:withCountOffset: können Sie jetzt die vollständige Undound 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
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.
6.1.6 Unit-Tests
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.
2 Meistens machen die Web-Proxys im Gegensatz zum Entwurfsmuster jedoch einfach auch nur viel Ärger.
620
621
6
Models, Layer, Animationen
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.
6.1
Modell und Controller
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.
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 File 폷 New 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
Testing Bundle.
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 6.8 Anlegen eines neuen Targets
622
623
6
Models, Layer, Animationen
6.1
Modell und Controller
XCTAssertEqual(1, 1U, @"Fehler"); // Falsch: int mit unsigned
XCTAssertEqual(1, 1.0, @"Fehler"); // Falsch: int mit double
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];
}
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 folgende Tests immer fehl:
624
- (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.
625
6
Models, Layer, Animationen
6.1
Modell und Controller
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 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 6.12 Meldung für nicht konfigurierte Tests
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.
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 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.13 Anzeige und Auswahl der Test-Targets
626
627
6
Models, Layer, Animationen
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.
628
6.1
Modell und Controller
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];
}
629
6
Models, Layer, Animationen
6.2
Als die Views das Laufen lernten
- (void)puzzleDidTilt:(NSNotification *)inNotification {
self.notification = inNotification;
}
XCTAssertTrue([self.puzzle tiltToDirection:PuzzleDirectionDown],
@"Can't tilt down.");
[self checkNotificationWithName:kPuzzleDidTiltNotification fromIndex:5 toIndex:9];
Listing 6.21 Registrierung für Benachrichtigungen in der Testklasse
Listing 6.23 Überprüfung der Benachrichtigungen
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];
630
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 Test Case Class. Häufig implementiert
man die Testfälle einer Klasse in jeweils einer eigenen Testklasse.
6.2 Als die Views das Laufen lernten
Für jedes Puzzleteil verwendet die App einen eigenen Imageview, wobei sie ein großes Bild in
die passenden Bilder für die Teile zerschneidet. Das ist sehr praktisch, wenn Sie das Bild austauschen wollen. Sie brauchen nur zwei Bilder in jeweils der Standard- und der Retina-Auflösung in den Ressourcen auszutauschen und sie nicht noch manuell zu zerschneiden.
Dazu enthält das Programm die Kategorie UIImage(Subimage) mit zwei Methoden. Über subimageWithRect: können Sie aus einem Bild einen rechteckigen Bereich in ein neues Bild
kopieren. Diese Methode verwendet dazu die Core-Graphics-Funktion CGImageCreateWithImageInRect, die genau diese Aufgabe erledigt.
Allerdings enthält ein Bild der Klasse UIImage seine Größe als geräteunabhängige Werte. Im
Beispielprojekt ist das Puzzlebild 300 × 300 Punkte groß – egal, ob es für die Standard- oder
die Retina-Auflösung ist. Koordinatenangaben in Core Graphics sind hingegen immer in
Pixeln. Sie müssen also die logische Bildgröße erst in die physikalische umrechnen, indem
Sie die Breite und die Höhe mit dem Skalierungsfaktor des Bildes multiplizieren. Diesen Faktor erhalten Sie über die Property scale der Klasse UIImage.
- (UIImage *)subimageWithRect:(CGRect)inRect {
CGFloat theScale = self.scale;
CGRect theRect = CGRectMake( theScale * CGRectGetMinX(inRect), theScale *
CGRectGetMinY(inRect), theScale * CGRectGetWidth(inRect), theScale *
CGRectGetHeight(inRect));
CGImageRef theImage = CGImageCreateWithImageInRect( self.CGImage, theRect);
UIImage *theResult = [UIImage imageWithCGImage:theImage scale:theScale
orientation:UIImageOrientationUp];
CGImageRelease(theImage);
return theResult;
}
Listing 6.24 Kopieren eines rechteckigen Bereiches in ein neues Bild
631
6
Models, Layer, Animationen
6.2
Die zweite Methode der Kategorie, splitIntoSubimagesWithRows:columns:, zerlegt ein Bild auf
theFromView.frame = [self frameForItemAtIndex:theToIndex];
theToView.frame = [self frameForItemAtIndex:theFromIndex];
Basis der Methode subimageWithRect: in gleich große, rechteckige Bilder und liefert das
Ergebnis in einem Array zurück.
}
- (NSArray *)splitIntoSubimagesWithRows:(NSUInteger)inRows columns:
(NSUInteger)inColumns {
CGSize theSize = self.size;
CGRect theRect = CGRectMake(0.0, 0.0, theSize.width / inColumns, theSize.height /
inRows);
NSMutableArray *theResult = [NSMutableArray arrayWithCapacity:inRows *
inColumns];
Listing 6.26 Aktualisierung der Puzzleteile
theSize = theRect.size;
for(NSUInteger theRow = 0; theRow < inRows; ++theRow) {
for(NSUInteger theColumn = 0; theColumn < inColumns; ++theColumn) {
theRect.origin.x = theSize.width * theColumn;
theRect.origin.y = theSize.height * theRow;
[theResult addObject: [self subimageWithRect:theRect]];
}
}
return [theResult copy];
}
Listing 6.25 Aufteilen eines Bildes für das Puzzle
Als die Views das Laufen lernten
6.2.1 Animationen mit Blöcken
Mit Hilfe von Blöcken können Sie anfangen, Ihre Views in Bewegung zu setzen. Dazu bietet
Ihnen die Klasse UIView eine Reihe von Klassenmethoden. Die Methoden operieren nicht auf
Objektebene, da Sie innerhalb einer Animation mehrere Views gleichzeitig verändern können. Die Animationen beschreiben Sie dabei über Property-Veränderungen der beteiligten
Views. Sie geben dazu einfach an, welchen Wert die Property am Ende des Animationsablaufs haben soll, und den Rest erledigt Cocoa Touch für Sie. Diese Animationsbeschreibung
erfolgt innerhalb eines Blocks. Die Arbeit mit Blöcken haben Sie ja bereits in Kapitel 2, »Die
Reise nach iOS«, kennengelernt. Im einfachsten Fall wollen Sie nur eine oder mehrere Eigenschaften Ihrer Views verändern und das über eine Animation visualisieren. Beispielsweise
vergrößern Sie folgendermaßen einen View animiert:
CGRect theFrame = theView.frame;
theFrame.size.width *= 2;
theFrame.size.height *= 2;
[UIView animateWithDuration:0.75 animations:^{
theView.frame = theFrame;
}];
Bei der Aktualisierung des Puzzles brauchen Sie jeweils nur den Stein an seine neuen Positionen zu setzen. Das können Sie erreichen, indem Sie der Property frame des Views für das
Puzzleteil einen entsprechenden Wert zuweisen. Um zu einem Puzzleteil den entsprechenden Subview zu finden, vertauscht die Methode die Views in der Reihenfolge. Dadurch entspricht die Position des Views im Subview-Array der Position des Puzzleteils im Array des
Modells.
- (void)puzzleDidTilt:(NSNotification *)inNotification {
NSDictionary *theInfo = inNotification.userInfo;
NSUInteger theFromIndex = [[theInfo objectForKey:kPuzzleFromIndexKey] intValue];
NSUInteger theToIndex = [[theInfo objectForKey:kPuzzleToIndexKey] intValue];
UIView *thePuzzleView = self.puzzleView;
UIView *theFromView = [thePuzzleView.subviews objectAtIndex:theFromIndex];
UIView *theToView = [thePuzzleView.subviews objectAtIndex:theToIndex];
[thePuzzleView exchangeSubviewAtIndex:theFromIndex withSubviewAtIndex:
theToIndex];
632
Listing 6.27 Animierte Vergrößerung eines Views
Durch diesen Code verdoppeln Sie jeweils die Breite und Höhe des Views, und Cocoa Touch
visualisiert diese Änderung, indem es den View über eine Bewegung aufzieht, die eine
Dreiviertelsekunde dauert. Dabei enthält dieser Code gegenüber einer nicht animierten
Version nur den Aufruf der Klassenmethode animateWithDuration:animations: und den
Block zusätzlich.
Parameterlose Blockfunktionen
Wenn eine Blockfunktion keinen Parameter erwartet, können Sie die Angabe der Parameterliste auch weglassen. In Listing 6.27 können Sie deshalb die Definition des Animationsblocks
von ^(void){ ... } auf ^{ ... } verkürzen.
Dieses Beispiel funktioniert allerdings nur bei ausgeschaltetem Autolayout. Mit eingeschaltetem Autolayout müssen Sie stattdessen die Restriktionen aktualisieren und im Animationsblock die Methode layoutIfNeeded des Views aufrufen. Dazu können Sie entweder die
Restriktionen austauschen oder aktualisieren.
633
6
Models, Layer, Animationen
NSLayoutConstraint *theWidthConstraint = ...;
NSLayoutConstraint *theHeightConstraint = ...;
theWidthConstraint.constant *= 2.0;
theHeightConstraint.constant *= 2.0;
[UIView animateWithDuration:0.75 animations:^{
[theView layoutIfNeeded];
}
Listing 6.28 Animation über Autolayout-Restriktionen
Sie können also eine Animation durch einen Methodenaufruf und eine Zeile im Animationsblock starten. Cocoa Touch erzeugt automatisch die notwendigen Animationen zu den
Änderungen, die Sie im Animationsblock an den Views vornehmen.
In vielen Fällen möchten Sie am Ende der Animation weiteren Code ausführen. Dafür gibt es
die Variante animateWithDuration:animations:completion: mit zwei Blockfunktionen als
Parameter:
[UIView animateWithDuration:0.75 animations:^{
theView.frame = theFrame;
}
completion:^(BOOL inFinished) {
theView.alpha = 0.5;
}];
Listing 6.29 Animation mit Block für das Animationsende
Die Animation ruft nach ihrer Beendigung den zweiten Block auf. Dabei gibt der boolesche
Parameter an, ob Cocoa Touch die Animation regulär beendet oder vor der Beendigung abgebrochen hat. In der Regel hat dieser Parameter also den Wert YES. Während auch dieser Code
die Größe des Views kontinuierlich ändert, setzt er die Transparenz nach der Animation in
einem Schritt auf 50 %, weil diese Änderung ja nicht im Animationsblock steht. Wollen Sie
hingegen den View synchron zur Größenänderung ausblenden lassen, müssen Sie die
Anweisung theView.alpha = 0.5; vom Completion- in den Animationsblock verschieben.
6.2
Als die Views das Laufen lernten
Wenn Sie die Animationen so stoppen, springt der View augenblicklich in den Endzustand. Alle
animierten Propertys des Views haben also die Werte, die Sie ihnen im Animationsblock zugewiesen haben.
Sie können folgende Propertys eines Views animieren:
1. Mit den Propertys frame und bounds verändern Sie die Position und die Größe des Views.
2. Sie können einem View eine affine Transformation – das ist eine 3×3-Matrix einer bestimmten Struktur – über die Property transform zuweisen. Mit Transformationen können Sie den View beispielsweise drehen, vergrößern und verschieben.
3. Über die Property center verschieben Sie den Mittelpunkt eines Views. Das hat nicht nur
Auswirkungen auf dessen Lage, sondern auch auf die Transformation des Views.
4. Ein- und Ausblendeffekte erreichen Sie durch eine Änderung des Wertes der Property
alpha.
5. Sogar die Hintergrundfarbe des Views können Sie über die Property backgroundColor animieren.
Mit diesen sieben animierbaren Propertys lässt sich schon eine Menge anstellen. Beispielsweise können Sie durch die animierte Änderung des Frames oder des Alphawertes Subviews
ein- und ausblenden lassen. Die Transformationsmatrix der Property transform ist eine CStruktur namens CGAffineTransform, die Sie über diverse Funktionen erstellen und modifizieren können. Sie hat anfänglich den Wert CGAffineTransformIdentity, der die Geometrie
des Views nicht verändert. Andere Matrizen lassen sich nun auf zwei Weisen erzeugen: Entweder erzeugen Sie eine neue Matrix zu vorgegebenen Werten, oder Sie ändern eine vorhandene Matrix.
Funktion
Beschreibung
CGAffineTransformMake
Erzeugt eine Transformationsmatrix über sechs
Werte: eine 2×2-Matrix und eine Verschiebung. Diese
Funktion verwendet man aber nur äußerst selten.
CGAffineTransformMakeRotation
Über diese Funktion erstellen Sie eine Drehung zu
einem angegebenen Winkel im Bogenmaß.
Abbrechen von Animationen
CGAffineTransformMakeScale
Für den Abbruch von View-Animationen gibt es keine dokumentierte Methode im UIKit. Da
diese Animationen jedoch auf Core Animation basieren, können Sie auf die Animationen eines
Views über seinen Layer zugreifen. Die folgende Anweisung stoppt alle laufenden Animationen des Views:
Erstellt eine Skalierung über zwei Werte entlang der
x- und der y-Achse. Mit den Wertpaaren
(1, –1) und (-1, 1) können Sie den View an der
x- beziehungsweise y-Achse spiegeln.
CGAffineTransformMakeTranslation
Diese Transformation verschiebt den View in
Richtung der angegebenen Werte.
[theView.layer removeAllAnimations];
Tabelle 6.3 Erzeugung von Transformationsmatrizen
634
635
6
Models, Layer, Animationen
6.2
Als die Views das Laufen lernten
theCard.index];
Transformationsmatrizen
Die Grundlagen für die Transformationsmatrizen oder affinen Transformationen sind ein Teilgebiet der linearen Algebra. Da die hier vorgestellten Funktionen für die meisten Anwendungsfälle ausreichen, verzichten wir auf eine genauere Darstellung.
Allerdings gibt es bei Rotationen einen kleinen Fallstrick: Die Darstellung als Matrix erlaubt
nur Drehungen im Winkelbereich von –π (–180°) bis π (180°). Winkelangaben außerhalb dieses
Bereiches führen allerdings nicht zu einem Programmfehler, sondern nur zu einer unerwarteten Drehung. Wenn Sie beispielsweise die Matrix für eine Dreivierteldrehung (3π / 2) gegen
den Uhrzeigersinn erzeugen, führt das zu einer Vierteldrehung im Uhrzeigersinn. Die Animation des Views nimmt also eine Abkürzung, wobei das Endergebnis allerdings gleich ist. Kontinuierliche Rotationen lassen sich so allerdings nur sehr schwer realisieren. Dafür verwenden
Sie besser Core Animation, worauf wir in Abschnitt 6.3, »Core Animation«, noch genauer eingehen.
Wenn Sie mehrere Transformationen, z. B. eine Rotation und eine Skalierung, auf einen View
anwenden möchten, können Sie auch bestehende Transformationsmatrizen miteinander
verknüpfen. Sie können dabei entweder jeweils eine der Operationen Verschieben (CGAffineTransformTranslate), Skalieren (CGAffineTransformScale) oder Drehen (CGAffineTransformRotate) auf eine Matrix anwenden. Alternativ können Sie auch über die Funktion CGAffineTransformConcat zwei Matrizen so miteinander verknüpfen, als hätten Sie die Transformationen nacheinander ausgeführt.
Das Memory-Spiel des Beispielprojekts Games lässt zwei gleiche Spielkarten durch eine
halbe Drehung mit gleichzeitiger Verkleinerung verschwinden, wodurch ein Strudeleffekt
entsteht. Dazu verknüpft es eine Rotation mit einer Skalierung (in Listing 6.30 hervorgehoben). Nach Beendigung der Animation versteckt es die beiden Views für die Karten und setzt
die Transformationsmatrix wieder auf die Identität, damit die Karten bei einem erneuten
Start des Spiels wieder die richtige Darstellung haben.
[UIView animateWithDuration:0.75 animations:^{
for(Card *theCard in theCards) {
CardView *theView = [self.memoryView.subviews objectAtIndex:
theCard.index];
theView.transform = CGAffineTransformScale(
CGAffineTransformMakeRotation(M_PI), 0.1, 0.1);
}
}
completion:^(BOOL inFinished) {
for(Card *theCard in theCards) {
CardView *theView = [self.memoryView.subviews objectAtIndex:
636
theView.hidden = theCard.solved;
theView.transform = CGAffineTransformIdentity;
}
}];
Listing 6.30 Verknüpfung von zwei Transformationen
Während der Animationsausführung ist die Verarbeitung von Touch-Events der animierten Views unterbrochen, so dass sie keine Eingaben empfangen können. In vielen Fällen ist
das auch nicht notwendig. Es gibt jedoch noch eine weitere Animationsmethode, und zwar
animateWithDuration:delay:options:animations:completion:, mit der Sie unter anderem dieses Verhalten beeinflussen können. Dazu besitzt diese Methode den Parameter options.
Außerdem können Sie über den Parameter delay den Ausführungsbeginn der Animation
verzögern.
Der Wert des options-Parameters ist eine Bitmenge, die Sie aus mehreren Konstanten
zusammensetzen können, die einen booleschen Wert darstellen. Wenn Sie mehrere Eigenschaften über die Optionen setzen wollen, müssen Sie die entsprechenden Konstanten über
den Operator »|« für das bitweise Oder verknüpfen.
Die Touch-Verarbeitung während der Animation lässt sich über die Konstante UIViewAnimationOptionAllowUserInteraction aktivieren.
Über View-Animationen können Sie Ihre Views auch kontinuierlich animieren, indem Sie
den Schalter UIViewAnimationOptionRepeat setzen; dann setzt die Animation am Ende der
Bewegung den View wieder auf den Ursprungszustand zurück und beginnt von vorn. Dabei
gibt dann der Parameter duration die Länge einer Wiederholung an, und die gesamte Animation läuft so lange, bis Sie sie explizit stoppen. Diese dauerhaften Animationen sollen häufig
auch den Rückweg, also den Übergang vom End- zum Anfangszustand, animieren. Mit dem
Schalter UIViewAnimationOptionAutoreverse können Sie dieses Verhalten einschalten.
Über den options-Parameter können Sie auch den Geschwindigkeitsverlauf in der Animation steuern. Tabelle 6.4 listet die vier dafür möglichen Konstanten mit einer Beschreibung
ihres Geschwindigkeitsverlaufs auf:
UIViewAnimationOptionCurve…
Die Animation …
…EaseInOut
… beschleunigt am Anfang und bremst am Ende ab.
…EaseIn
… beschleunigt am Anfang und läuft dann bis zum Ende
mit einer konstanten Geschwindigkeit.
Tabelle 6.4 Geschwindigkeitsverläufe in der Animation
637
6
Models, Layer, Animationen
6.2
UIViewAnimationOptionCurve…
Die Animation …
…EaseOut
… startet mit einer konstanten Geschwindigkeit und
verlangsamt sich am Ende.
…Linear
… hat über den gesamten Verlauf die gleiche
Geschwindigkeit.
Tabelle 6.4 Geschwindigkeitsverläufe in der Animation (Forts.)
Projektinformation
Das Beispielprojekt Animation, das Sie im Github-Repository zum Buch im Unterverzeichnis
https://github.com/Cocoaneheads/iPhone/tree/Ausgabe_iOS8/Apps/iOS6/Animation finden,
vermittelt einen Eindruck von den verschiedenen Animationsverläufen. Sie können dort über
ein Segmented-Control die Animationskurve auswählen und eine Animation starten.
Durch wiederholte Animationen können Sie sehr leicht die Wackelanimation erzeugen, die
Sie sicherlich aus dem Springboard des iPhones kennen, wenn Sie dessen Apps bearbeiten
möchten.3 Diese Animation ist einfach eine schnelle Hin- und Zurückdrehung des Views um
einen kleinen Winkel, für die Sie die Repeat- und Autoreverse-Option einschalten müssen.
Außerdem sollten Sie die Ease-in-Ease-out-Animationskurve verwenden, damit die Bewegung nicht zu abgehackt aussieht.
Damit die Animation außerdem gleichmäßig zu beiden Seiten dreht, müssen Sie den View
vor der Animation mit dem negativen Winkel in die Gegenrichtung drehen, was Sie nach der
Beendigung allerdings wieder zurücknehmen. Den kompletten Code für diese Animation
sehen Sie in Listing 6.31:
CGFloat theAngle = 2.5 * M_PI / 180;
theView.transform = CGAffineTransformMakeRotation(-theAngle);
[UIView animateWithDuration:0.05 delay:0.0 options:UIViewAnimationOptionRepeat |
UIViewAnimationOptionAutoreverse | UIViewAnimationOptionCurveEaseInOut
animations:^{
theView.transform = CGAffineTransformMakeRotation(theAngle);
}
completion:^(BOOL inFinished) {
theView.transform = CGAffineTransformIdentity;
}];
Animationen ohne Blockfunktionen
Viele Programmbeispiele im Netz starten Animationen über Aufrufe der Klassenmethoden
beginAnimations:context: und commitAnimations und verwenden weitere Klassenmethoden
zwischen diesen Aufrufen, um Animationsoptionen zu setzen. Die Animation aus Listing 6.27
sieht ohne Blockfunktionen beispielsweise so aus:
CGRect theFrame = theView.frame;
theFrame.size.width *= 2;
theFrame.size.height *= 2;
[UIView beginAnimations:@"resize" context:NULL];
[UIView setAnimationDuration:0.75];
theView.frame = theFrame;
[UIView commitAnimations];
Apple hat dieses Verfahren der Animationserzeugung allerdings schon vor geraumer Zeit als
veraltet markiert, und Sie sollten lieber die Klassenmethoden mit den Blockfunktionen verwenden.
6.2.2 Transitionen
Mit den vorgestellten Animationen können Sie das Aussehen eines oder mehrerer Views
gleichzeitig ändern. Für das Memory-Spiel aus dem Beispielprojekt Games soll eine Animation das Umdrehen der Karten simulieren. Das können Sie mit Viewtransitionen erreichen.
Transitionen sind Animationen zum gleichzeitigen Ein- und Ausblenden von Views. Die Animation verbindet dabei das Erscheinen und das Verschwinden der Views zu einem Effekt. Es
gibt folgende Transitionen:
UIViewAnimationOptionTransition…
Die Animation …
…None
… wird nicht beeinflusst, kein Animationseffekt.
Das ist der Standardwert.
…FlipFromLeft
… dreht die Views um deren vertikale Achse um
180° – die linke Seite nach vorn und die rechte nach
hinten.
…FlipFromRight
… dreht die Views um deren vertikale Achse um
180° – die rechte Seite nach vorn und die linke nach
hinten.
…FlipFromTop
… dreht die Views um deren horizontale Achse um
180°; dabei bewegt sich die obere Kante nach vorn
und die untere nach hinten.
Listing 6.31 Wackelanimation wie beim Bearbeiten des Springboards
3 Falls Sie sie nicht kennen: Drücken Sie einfach ein Icon im Springboard so lange, bis alle Icons zu wackeln
anfangen.
638
Als die Views das Laufen lernten
Tabelle 6.5 Transitionstypen
639
6
Models, Layer, Animationen
6.2
UIViewAnimationOptionTransition…
Die Animation …
…FlipFromBottom
… dreht die Views um deren horizontale Achse um
180°; dabei bewegt sich die obere Kante nach vorn
und die untere nach hinten.
…CurlUp
… blättert den auszublendenden View wie ein Kalenderblatt nach oben weg.
…CurlDown
… blättert den einzublendenden View von oben ein.
…CrossDissolve
… blendet den alten View aus und den neuen View
durch einen Überblendeffekt ein.
Tabelle 6.5 Transitionstypen (Forts.)
Sie können eine Transition über die Klassenmethode transitionWithView:duration:options:
animations:completion: von UIView starten. Dabei geben Sie im ersten Parameter den View
an, dessen Subviews Sie in die Transition einbeziehen möchten. Die Transition animiert alle
Views, die Sie anzeigen, verstecken, zum View hinzufügen oder aus ihm entfernen. Die anderen Parameter verhalten sich genau wie bei den bereits vorgestellten Animationsmethoden.
Die Flipanimationen eignen sich hervorragend, um das Umdrehen der Karten im MemorySpiel der Beispielapplikation zu animieren. Eine Karte im Memory besteht aus drei Views.
Der äußere View der Klasse CardView dient als Container, der jeweils einen View für die Vorder- und Rückseite der Karte enthält. Die Vorderseite zeigt ein farbiges Vieleck, während die
Rückseite ein einheitliches Punktmuster anzeigt. Von diesen beiden Views ist pro Karte allerdings immer nur einer sichtbar. Das Umdrehen einer Karte vertauscht einfach die Sichtbarkeit der beiden Karten; dabei steuern Sie die Transitionen der Karten über die Methode
showFrontSide:withAnimationCompletion:.
- (FrontView *)frontView {
return self.subviews.lastObject;
}
- (BOOL)showsFrontSide {
return !self.frontView.hidden;
}
- (void)setShowsFrontSide:(BOOL)inShowFront {
[[self.subviews objectAtIndex:0] setHidden:inShowFront];
self.frontView.hidden = !inShowingFront;
}
Als die Views das Laufen lernten
inFinished))inCompletion {
UIViewAnimationOptions theTransition = inShow ?
UIViewAnimationOptionTransitionFlipFromLeft :
UIViewAnimationOptionTransitionFlipFromRight;
[UIView transitionWithView:self duration:0.75 options:theTransition |
UIViewAnimationOptionAllowUserInteraction animations:^{
self.showsFrontSide = inShow;
}
completion:inCompletion];
}
Listing 6.32 Umdrehen der Memory-Karten über eine Transition
Der Animationsblock verändert die Property hidden der beiden Seiten der Karte, so dass sie
jeweils nur einen View zeigt. Die Transition erzeugt daraus eine Animation, die wie ein
Umdrehen der Karten aussieht. Für das Anzeigen und Verstecken der Vorderseite verwendet
die Methode dabei die entgegengesetzten Transitionen.
Animierbare Propertys
In Listing 6.32 sieht es so aus, als hätte die Klasse CardView mit der Property showsFrontSide
eine neue animierbare Property. Tatsächlich ändert die Methode setShowsFrontSide: jedoch
nur die Property hidden der Views.
Diese Property ist zwar auch nicht für gewöhnliche Animationen vorgesehen, Transitionen lassen sich allerdings darüber auslösen. Über Core Animation können Sie auch animierbare Propertys in Ihren Viewklassen implementieren.
Wenn Sie in einer Transition auch noch Views animieren wollen, können Sie dazu die Option
UIViewAnimationOptionAllowAnimatedContent setzen. Dann setzt Cocoa Touch alle Änderungen an Views im Animationsblock der Transition auch in Animationen um.
6.2.3 Zur Animation? Bitte jeder nur einen Block!
Wenn Sie mehrere Animationen hintereinander ausführen wollen, reicht dafür das Hintereinanderschreiben mehrerer Animationsanweisungen nicht aus. Die Animationen laufen
dann trotzdem gleichzeitig ab. Das liegt einerseits daran, dass Cocoa Touch die Animationen
auch aus der Runloop heraus startet. Andererseits warten die Animationsmethoden auch
nicht auf den Ablauf der Animationen.
Bei Animationen können Sie dafür allerdings den Parameter delay verwenden. Wenn Sie ihn
bei der zweiten Animation auf die Dauer der ersten Animation setzen, startet die zweite nach
dem Ende der ersten.
- (void)showFrontSide:(BOOL)inShow withAnimationCompletion: (void (^)(BOOL
640
641
6
Models, Layer, Animationen
6.3
Listing 6.33 enthält dazu ein Beispiel für drei aufeinanderfolgende Animationen. Es startet
die erste ohne Verzögerung mit einer Dauer von einer Dreiviertelsekunde. Entsprechend
startet der Code die zweite Animation mit einer Verzögerung von 750 Millisekunden. Die
dritte Animation bekommt hingegen eine Verzögerung von 1.250 Millisekunden, die sich
aus der Summe der benötigten Zeiten für die ersten beiden Animationen errechnet.
Core Animation
[self showCardView:inShow atIndex:inIndex + 1];
if(inIndex == 0 && inShow) {
[self showCardView:NO atIndex:0];
}
}];
}
[UIView animateWithDuration:0.75 animations:^{
// erste Animationsphase konfigurieren
}];
[UIView animateWithDuration:0.5 delay:0.75 options:0 animations:^{
// zweite Animationsphase konfigurieren
}
completions:NULL];
[UIView animateWithDuration:0.25 delay:1.25 options:0 animations:^{
// dritte Animationsphase konfigurieren
}
completions:NULL];
Listing 6.33 Starten von aufeinanderfolgenden Animationen
Bei Transitionen können Sie keine Verzögerung angeben. Hier können Sie jedoch den Completion-Block nutzen. In diesem Block können Sie jeweils die anschließende Transition starten, so dass Sie auch hier eine sequenzielle Ausführung der Transitionen erhalten.
Das Memory-Spiel des Beispielprojekts besitzt einen Hilfeknopf, mit dem Sie die verdeckten
Karten der Reihe nach kurz aufdecken können. Dazu besitzt die Klasse MemoryViewController
die rekursive Methode showCardView:atIndex:, die den Inhalt der verdeckten Karten nacheinander kurz anzeigt.
- (void)showCardView:(BOOL)inShow atIndex:(NSUInteger)inIndex {
NSArray *theViews = self.memoryView.subviews;
UIViewAnimationOptions theOptions = inShow ?
UIViewAnimationOptionTransitionCurlUp :
UIViewAnimationOptionTransitionCurlDown;
if(inIndex < theViews.count) {
Card *theCard = [self.memory.cards objectAtIndex:inIndex];
CardView *theView = [theViews objectAtIndex:inIndex];
[UIView transitionWithView:theView duration:0.25 options:theOptions
animations:^{
theView.showsFrontSide = inShow || theCard.showsFrontSide;
}
completion:^(BOOL inFinished) {
642
}
Listing 6.34 Animierte Hilfe des Memory-Spiels
Der erste Parameter gibt an, ob die Methode die Karte an der Indexposition zeigen oder verdecken soll. Das Auf- und Zudecken der Karte geschieht dabei auch über die Property showsFrontSide und eine Curl-up- beziehungsweise Curl-down-Transition. Der Completion-Block
der Transition startet dabei jeweils die Transition der nächsten Karte, so dass dadurch eine
Kette von Transitionen entsteht. Außerdem startet der Completion-Block für die erste Karte
eine zweite Transitionskette zum Verdecken der Karten. Dazu verwendet er ebenfalls die
Methode showCardView:atIndex:, diesmal allerdings mit dem Wert NO für den ersten Parameter, um die Karten zu verdecken.
6.3 Core Animation
Auf den ersten Blick wirken die View-Animationen ein bisschen wie Magie. Durch einen
zusätzlichen Methodenaufruf verwandeln Sie eine statische Veränderung des Views in einen
eleganten Übergang, wobei Sie auch noch eine große Palette möglicher Animationen zur
Verfügung haben. Da fragt sich doch der neugierige Programmierer, wie das funktioniert.
Die Antwort darauf findet sich eine Ebene tiefer.
Core Animation ist eine Sammlung von Klassen zur Darstellung, Projektion und Animation
von Grafiken. Der Name Core Animation ist dabei vielleicht etwas verwirrend, da Sie Core
Animation auch ohne Animationen sinnvoll verwenden können.
Core Animation verwenden
Wenn Sie die Klassen aus Core Animation in Ihren Apps verwenden möchten, müssen Sie das
QuartzCore-Framework einbinden. Dazu wählen Sie im Projektnavigator das Projekt aus und
danach unter Targets das entsprechende Target. Dort gehen Sie in die Rubrik Linked Frameworks and Libraries unter den Reiter General. Wenn Sie den Plusknopf am unteren Rand
der Liste anklicken, können Sie das Framework über den Eintrag QuartzCore.framework
auswählen und zu dem Target hinzufügen. Außerdem müssen Sie den entsprechenden Header über
#import <QuartzCore/QuartzCore.h>
in Ihre Quelldateien importieren, damit der Compiler Ihren Programmcode übersetzen kann.
643
6
Models, Layer, Animationen
6.3
6.3.1 Layer
Ein wichtiger Bestandteil von Core Animation sind Layer, die auch die Views des UIKits für
ihre Darstellung verwenden. Ein Layer ist eine Zeichenebene, die Inhalte für die Anzeige
bereitstellt, und seine Basisklasse ist CALayer. Dabei ist jedem View ein Layer zugeordnet, den
Sie über die Property layer erhalten. Nach der Erzeugung eines Views ist sein Layer fest, und
Sie können dem View keinen neuen Layer zuweisen. Das ist im Allgemeinen auch nicht
nötig, da Sie ja beliebige Grafiken über den View zeichnen können (siehe Kapitel 3, »Sehen
und anfassen«).
Die Klasse UIView hat allerdings die Klassenmethode layerClass, über die sie die Klasse ihres
Layers bestimmt; normalerweise ist das die Klasse CALayer. Sie können in Ihren eigenen
Unterklassen jedoch diese Methode überschreiben, um so die Layer-Klasse des Views festlegen zu können. Das Beispielprojekt Pie enthält die Klasse PieView, die für die Darstellung die
Layer-Klasse PieLayer verwendet. Listing 6.35 enthält die Implementierung der Methode
layerClass, um diese Zuordnung zu realisieren.
Core Animation
Jeder View hat zwar einen Layer, nicht jeder Layer muss dagegen zu einem View gehören. Sie
können also in einen Layer beliebig viele Sublayer einfügen. Die Verwaltung der Sublayer
erfolgt dabei analog zu Views. Sie erhalten alle Sublayer eines Layers über die Property sublayers. Durch die Methode addSublayer: können Sie neue Sublayer zu einem Layer hinzufügen, und über die Property superlayer haben Sie Zugriff auf den darüberliegenden Layer.
Layer haben noch eine Reihe weiterer Eigenschaften. Sie finden im Layer viele Propertys, die
Sie schon von der Klasse UIView kennen. Teilweise haben sie jedoch auch andere Namen. Beispielsweise heißt die analoge Methode zu clipsToBounds des Views im Layer masksToBounds,
und das Gegenstück zu der View-Property alpha im Layer ist opacity.
Sehr praktisch ist beispielsweise die Property cornerRadius der Klasse CALayer. Damit können
Sie die Ecken eines Layers abrunden. Dafür müssen Sie den Property-Wert größer als 0 setzen. Außerdem können Sie einen Schatten, eine Rahmenfarbe und eine Rahmenbreite wie
beispielsweise in Listing 6.36 festlegen:
theLayer.cornerRadius = 10.0;
theLayer.borderColor = [UIColor blueColor].CGColor;
theLayer.borderWidth = 2.0;
theLayer.shadowColor = [UIColor blackColor].CGColor;
theLayer.shadowOffset = CGSizeMake(5.0, 5.0);
theLayer.shadowOpacity = 0.5;
+ (Class)layerClass {
return [PieLayer class];
}
Listing 6.35 Festlegen der Layer-Klasse eines Views
Listing 6.36 Erzeugung eines Layer-Rahmens
Projektinformation
Den Quellcode des Beispielprojekts Pie finden Sie im Github-Repository zum Buch im Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Ausgabe_iOS8/Apps/iOS5/Pie.
Layer können, analog zu Views, Sublayer haben. Wenn ein View Subviews hat, dann sind
deren Layer die Sublayer des Layers des Views. Eine grafische Veranschaulichung dafür finden Sie in Abbildung 6.16.
Layer
View
Sublayer
Subview
Subview
Sublayer
Abbildung 6.16 View- und Layer-Hierarchie
644
Sublayer
Sublayer
Core Animation und das UIKit
Wie Sie an Listing 6.36 sehen, verwenden Layer Core-Graphics-Farben und keine UIColorObjekte. Da das UIKit auf Core Animation basiert, dürfen die Komponenten aus Letzterem
auch nicht Komponenten des UIKits verwenden, da das zu zyklischen Abhängigkeiten zwischen diesen Frameworks führte. Core Animation verwendet also immer die entsprechenden
Komponenten aus Core Graphics: beispielsweise CGColor, CGImage, CGFont und CGPath anstatt
UIColor, UIImage, UIFont beziehungsweise UIBezierPath. Die Klassen des UIKits erlauben in
der Regel auch einen Zugriff auf die entsprechende Core-Graphics-Komponente.
Wenn Sie die Propertys wie dort setzen, erhalten Sie einen Rahmen um den Layer, wie ihn
Abbildung 6.17 darstellt. Sie können diese Propertys natürlich auch bei dem Layer eines
Views setzen, um die Ecken eines rechteckigen Views abzurunden. Das ist eine einfache Möglichkeit, die Gestaltung der Oberflächen Ihrer Apps aufzulockern.
Ein Layer kann auch ein Delegate haben, das bei den Standard-Layern der Views immer der
View ist. Sie dürfen weder diesen Layern ein anderes Delegate zuweisen noch den View als
Delegate für einen anderen Layer verwenden. Bei allen anderen Layern können Sie hingegen
das Delegate-Objekt frei wählen und über die Property delegate setzen.
645
6
Models, Layer, Animationen
6.3
Core Animation
Der dritte Weg zur Bereitstellung des Layer-Inhalts ist das Überschreiben der Methode drawInformelles Protokoll
InContext: in einer Unterklasse von CALayer. Auch hier bekommen Sie den Kontext zum
Es gibt kein formales Protokoll für das Layer-Delegate. Die Deklaration befindet sich hingegen
in der Kategorie NSObject(CALayerDelegate) in der Header-Datei der Layer-Klasse. Diese Art,
Delegate-Methoden zu deklarieren, heißt auch informelles Protokoll, und Sie finden die Dokumentation dieser Methoden unter »CALayerDelegate Informal Protocol Reference«.
Zeichnen als Parameter übergeben.
Es gibt drei mögliche Wege, den Inhalt eines Layers bereitzustellen. Über die Property contents können Sie dem Layer ein Core-Graphics-Bild (CGImageRef) zuweisen. Eine solche Referenz erhalten Sie beispielsweise über die Property CGImage eines Bildes der Klasse UIImage.
Apple empfiehlt, diese Zuordnung in der Delegate-Methode displayLayer: vorzunehmen. In
Listing 6.38 sehen Sie ein Beispiel, wie Sie den Inhalt über ein Bild bereitstellen können.
- (void)displayLayer:(CALayer *)inLayer {
UIImage *theImage = [UIImage imageNamed:@"image.png"];
Das Beispielprojekt Pie stellt ein Kreissegment dar, dessen Ausschnitt Sie über einen Schieberegler verändern können (siehe Abbildung 6.17). Die Darstellung des Segments basiert dabei
auf einem Layer mit folgender Implementierung der Methode drawInContext:.
- (void)drawInContext:(CGContextRef)inContext {
CGRect theBounds = self.bounds;
CGSize theSize = theBounds.size;
CGFloat thePart = self.part;
CGPoint theCenter = CGPointMake(CGRectGetMidX(theBounds),
CGRectGetMidY(theBounds));
CGFloat theRadius = fminf(theSize.width, theSize.height) / 2.0 – 5.0;
CGFloat theAngle = 2 * (thePart – 0.25) * M_PI;
theLayer.contents = theImage.CGImage;
CGContextSaveGState(inContext);
CGContextSetFillColorWithColor(inContext, [UIColor redColor].CGColor);
CGContextMoveToPoint(inContext, theCenter.x, theCenter.y);
CGContextAddArc(inContext, theCenter.x, theCenter.y, theRadius, -M_PI / 2.0,
theAngle, NO);
CGContextAddLineToPoint(inContext, theCenter.x, theCenter.y);
CGContextFillPath(inContext);
CGContextRestoreGState(inContext);
}
Listing 6.37 Setzen der »contents«-Property eines Layers
Der Layer-Inhalt lässt sich hingegen auch durch Zeichenoperationen erzeugen. Sie können
dafür entweder eine Delegate-Methode verwenden oder eine Unterklasse erstellen. Für die
Delegation implementieren Sie die Methode drawLayer:inContext:, wobei Sie den Kontextparameter für Ihre Zeichenoperationen verwenden:
- (void)drawLayer:(CALayer *)inLayer inContext:(CGContextRef)inContext {
CGContextSaveGState(inContext);
// beliebige Zeichenoperationen
CGContextRestoreGState(inContext);
}
}
Listing 6.39 Zeichnen des Kreissegments im Layer
Listing 6.38 Inhalt eines Layers über Delegate-Methode zeichnen
Cocoa Touch ruft die Methode drawRect: über drawLayer:inContext: auf. Sie haben also
bereits – ohne es zu ahnen – Layer-Inhalte über Delegation erzeugt.
»UIView« und »drawLayer:inContext:«
Wenn Sie den View-Iinhalt über die Delegate-Methode anstatt mit drawRect: zeichnen möchten, dann müssen Sie trotzdem die Methode drawRect: überschreiben. Ansonsten ruft Cocoa
Touch die Delegate-Methode nicht auf. Es reicht indes aus, wenn Sie die Methode leer lassen.
Abbildung 6.17 Das »Pie«-Beispielprogramm
646
647
6
Models, Layer, Animationen
Die Property part enthält die Größe des Kreissegments als Wert zwischen 0 und 1. Ein Layer
kann über die Methoden valueForKey: und setValue:forKey: beliebige Werte speichern, und
die Klasse PieLayer speichert den Wert für die Property part auch über diese Methoden. Die
Deklaration dieser Property in der Klasse PieLayer erfolgt zwar über ein gewohntes
@property (nonatomic) CGFloat part;
die Implementierung erfolgt hingegen über die Anweisung @dynamic part;, wodurch der
Layer die beiden Methoden valueForKey: und setValue:forKey: für den Getter beziehungsweise Setter mit dem Schlüssel »part« verwendet.
Achtung bei automatisch synthetisierten Propertys
Mit Xcode 4.5 hat sich das Verhalten des Compilers geändert: In den älteren Versionen konnten Sie die @dynamic-Anweisung auch einfach weglassen. Der Compiler spuckte dann zwar
eine Warnung aus, das Programm funktionierte indes trotzdem. Xcode 4.5 nimmt bei fehlender Implementierungsanweisung für eine Property an, dass die Applikation sie synthetisieren
soll. Die @dynamic-Anweisung ist ab Xcode 4.5 also notwendig.
Durch die dynamische Implementierung der Property ruft die Laufzeitumgebung immer,
wenn Sie lesend oder schreibend auf part zugreifen, die Methoden valueForKey: beziehungsweise setValue:forKey: mit @"part" als Schlüssel auf. Dabei packt sie den Fließkommawert
immer schön aus dem NSNumber-Objekt aus beziehungsweise in ein solches Objekt ein.
Layer-Eigenschaften über Propertys
Legen Sie die Propertys eines Layers möglichst immer dynamisch an. Sie können zwar auch
synthetisierte Propertys verwenden oder eigene Implementierungen dafür schreiben. Allerdings dürfen Sie in der Implementierung nicht die Key-Value-Coding-Methoden mit dem Property-Namen als Schlüssel verwenden. Die Implementierung
- (CGFloat)part {
return [[self valueForKey:@"part"] floatValue];
}
führt zu einer Endlosrekursion, da der Aufruf von valueForKey: wieder die Methode part aufruft. Sie bekommen indes weiteren Ärger, wenn Sie diese Propertys animieren wollen. Also
nutzen Sie lieber das Key-Value-Coding. Das ist ein Angebot, das Sie einfach nicht ablehnen
können.
Natürlich sollte der Layer seine Propertys auch mit Werten vorbelegen. Dafür stellt die Klasse
CALayer die Klassenmethode defaultValueForKey: zur Verfügung, die Sie überschreiben können. Sie liefert für die Schlüssel des Layers entweder den Standardwert zurück oder reicht
den Aufruf an die Methode der Oberklasse weiter. Da diese Methode den Rückgabetyp id hat,
muss sie den Standardwert für die Property part als Objekt liefern.
648
6.3
Core Animation
static NSString * const kPartKey = @"part";
+ (id)defaultValueForKey:(NSString *)inKey {
return [kPartKey isEqualToString:inKey] ? @0.0f : [super defaultValueForKey:
inKey];
}
Listing 6.40 Standardwert für die Property »part«
Wenn Sie jetzt die Klasse PieLayer verwenden, sehen Sie noch nichts – zumindest kein Tortenstück. Das könnte daran liegen, dass die Größe des Segments den Wert 0 hat; jedoch
selbst mit einem anderen Standardwert für part gibt’s immer noch keinen Kuchen, und die
Fläche bleibt immer noch weiß.
Das kommt daher, dass niemand dem Layer gesagt hat, dass er etwas zeichnen soll. Sie können das erreichen, indem Sie ihm die Nachricht setNeedsDisplay schicken. Das müssen Sie
immer machen, wenn Sie den Wert für part verändern. Das ist allerdings sehr unpraktisch,
da Sie dafür entweder die Methode setPart: implementieren oder KVO verwenden müssten.
Um dieses Problem zu umgehen, bietet die Layer-Klasse über die Klassenmethode needsDisplayForKey: eine einfachere Möglichkeit an. Core Animation zeichnet den Layer bei einer
Änderung eines Property-Wertes automatisch neu, wenn die Methode für den PropertyNamen YES liefert. Sie können dazu diese Methode in Ihren Klassen einfach wie in Listing 6.41
überschreiben.
+ (BOOL)needsDisplayForKey:(NSString *)inKey {
return [kPartKey isEqualToString:inKey] || [super needsDisplayForKey:inKey];
}
Listing 6.41 Layer bei Änderung des Property-Wertes neu zeichnen
Layer-Propertys gesucht
Core Animation ruft die Methode needsDisplayForKey: einmal für jede Property der LayerKlasse auf. Sie sollten auch deshalb die Layer-Eigenschaften als dynamische Property deklarieren. Für Eigenschaften, die Sie nur über Getter und Setter deklariert haben, ruft Core Animation needsDisplayForKey: hingegen nicht auf. Überschreiben Sie also lieber die Methode
needsDisplayForKey:.
Irgendwie klingt das schon wieder nach einem Angebot, das Sie nicht ablehnen können.
Durch die Implementierung der dynamischen Property und der drei Methoden defaultValueForKey:, needsDisplayForKey: und drawInContext: zeigt der Layer nun auch ein Tortenstück an.
649
6
Models, Layer, Animationen
6.3.2 Vordefinierte Layer-Klassen
Core Animation stellt auch fertige Unterklassen von CALayer für verschiedene Anwendungsfälle bereit, deren Handhabung das Beispielprojekt Layer zeigt. Die Ausgabe der beschriebenen Layer sehen Sie in Abbildung 6.18.
6.3
Core Animation
Gradient verläuft von oben nach unten, was sich über den Startpunkt (0,5, 0) und den Endpunkt (0,5, 1) realisieren lässt.
startPoint
startPoint
endPoint
endPoint
Abbildung 6.19 Gradientenverlauf zwischen Start- und Endpunkt
Das Beispielprojekt legt einen Gradienten-Layer mit drei Farben und einem Verlauf von
oben links nach unten rechts folgendermaßen an:
Abbildung 6.18 Ausgabe der Layer-App
Projektinformation
Den Quellcode des Beispielprojekts Layer finden Sie im Github-Repository zum Buch im Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Ausgabe_iOS8/Apps/iOS5/Layer.
Mit einem Layer der Klasse CAGradientLayer können Sie Farbverläufe darstellen, die auf zwei
oder mehr Farben basieren. Die Farben übergeben Sie an den Layer als CGColorRef-Werte in
einem NSArray. Der Farbverlauf verläuft entlang einer Linie, die Sie über die Propertys startPoint und endPoint festlegen.
Relative Positionsangaben bei Layern
Positionen oder Rechtecke innerhalb eines Layers müssen Sie in der Regel durch Werte zwischen 0 und 1 beschreiben und nicht durch absolute Koordinaten. Den Ankerpunkt eines Layers können Sie beispielsweise über die Property anchorPoint festlegen. Standardmäßig ist das
der Mittelpunkt des Layers, und diese Property hat somit den Wert (0,5, 0,5).
Abbildung 6.19 enthält zwei Gradientenverläufe von Weiß nach Schwarz mit unterschiedlichen Start- und Endpunkten. Die Lage der Punkte verdeutlicht dabei jeweils eine Verbindungslinie auf dem Gradienten. Der linke Verlauf startet in der Ecke links oben und endet
unten rechts. Der Startpunkt hat also den Wert (0, 0) und der Endpunkt (1, 1). Der rechte
650
CAGradientLayer *theLayer = [CAGradientLayer layer];
NSArray *theColors = @[(id)[UIColor redColor].CGColor,
(id)[UIColor greenColor].CGColor, (id)[UIColor blueColor].CGColor];
self.frame = inFrame;
theLayer.colors = theColors;
theLayer.startPoint = CGPointMake(0.0, 0.0);
theLayer.endPoint = CGPointMake(1.0, 1.0);
Listing 6.42 Anlegen eines Layers mit Gradientenverlauf
Über Layer der Klasse CATextLayer können Sie Texte darstellen. Die Implementierung des
Layers beruht auf Core Text. Das ist ein Low-Level-Framework für die Ausgabe von Texten.
Sie können über diesen Layer auch Texte mit Hervorhebungen (z. B. fett oder kursiv) und
unterschiedlichen Schriftarten ausgeben. In Listing 6.43 sehen Sie ein Beispiel, wie Sie einen
Textlayer anlegen können. Dabei sorgt die Property wrapped dafür, dass der Text im Layer
umbricht.
CATextLayer *theLayer = [CATextLayer layer];
CGAffineTransform theIdentity = CGAffineTransformIdentity;
CTFontRef theFont = CTFontCreateWithName( (CFStringRef)@"Courier", 24.0,
&theIdentity);
651
6
Models, Layer, Animationen
theLayer.frame = inFrame;
theLayer.font = theFont;
theLayer.fontSize = 20.0;
theLayer.backgroundColor = [UIColor whiteColor].CGColor;
theLayer.foregroundColor = [UIColor blackColor].CGColor;
theLayer.wrapped = YES;
theLayer.string =
@"Die heiße Zypernsonne quälte Max und Victoria ja böse auf dem Weg
bis zur Küste.";
CFRelease(theFont);
Listing 6.43 Anlegen eines Textlayers
6.3
Core Animation
theLayer.frame = inFrame;
theLayer.backgroundColor = [UIColor whiteColor].CGColor;
theLayer.strokeColor = [UIColor blackColor].CGColor;
theLayer.fillColor = [UIColor clearColor].CGColor;
theLayer.lineWidth = 1.0;
CGPathMoveToPoint(thePath, NULL, 0.0, f(0.0) + theOffset);
for(CGFloat x = 1.0; x < theWidth; x += 1.0) {
CGPathAddLineToPoint(thePath, NULL, x, f(x * M_PI / theWidth) + theOffset);
}
theLayer.path = thePath;
CGPathRelease(thePath);
Listing 6.45 Erzeugung eines Shapelayers mit einem Pfad
Die Klasse CAScrollLayer erlaubt die Anzeige von Inhalten, die größer sind als der verfügbare
Bereich für die Darstellung. Sie zeigen also wie ein UIScrollView immer nur einen Ausschnitt
des Inhalts an. Im Gegensatz zum View verarbeitet der Layer hingegen keine Scrollgesten,
und er zeigt auch keine Scrollbalken an.
Sie können den angezeigten Ausschnitt des Scrolllayers über die Methoden scrollToPoint:
und scrollToRect: festlegen. Über scrollToPoint: legen Sie die linke obere Ecke des Ausschnitts für die Anzeige fest. Dabei geben Sie den Punkt in absoluten Koordinaten zum
Layer-Inhalt an. Bei scrollToRect: übergeben Sie ein Rechteck aus dem Koordinatensystem
des Inhalts. Der Scrolllayer stellt dann einen Ausschnitt des Inhalts dar, der dieses Rechteck
enthält. Listing 6.44 legt einen Scrolllayer an, der einen Textlayer enthält.
CAScrollLayer *theLayer = [CAScrollLayer layer];
CATextLayer *theTextLayer = ...;
theLayer.frame = inFrame;
[theLayer addSublayer:theTextLayer];
theLayer.scrollMode = kCAScrollVertically;
Listing 6.44 Anlegen eines Scrolllayers
Ein Layer der Klasse CAShapeLayer stellt einen Core-Graphics-Pfad dar. Dabei können Sie diesen Pfad über die Property path setzen. Auch für die anderen üblichen Verdächtigen gibt es
entsprechende Propertys. Sie setzen beispielsweise die Füllfarbe über fillColor und die Linienfarbe über strokeColor.
CAShapeLayer *theLayer = [CAShapeLayer layer];
CGMutablePathRef thePath = CGPathCreateMutable();
CGFloat theOffset = CGRectGetHeight(inFrame) / 2.0;
CGFloat theWidth = CGRectGetWidth(inFrame);
652
Wie Sie in Listing 6.45 sehen, erzeugen Sie Core-Graphics-Pfade analog zu den Zeichenpfaden
in einem Grafikkontext. Im Gegensatz zu den Kontextpfaden können Sie die Pfade der
Typen CGPathRef und CGMutablePathRef mehrmals verwenden und auch als Werte in Ihren
Objekten speichern. Auch der Grafikkontext kann mit diesen Pfaden umgehen; er unterstützt verschiedene Funktionen, mit denen Sie Pfade zeichnen, füllen oder zum Kontext hinzufügen können.
6.3.3 Der Layer mit der Maske
Layer können Sie außerdem zum Beschneiden des Inhalts anderer Layer verwenden. Dazu
besitzt die Klasse CALayer die Property mask, der Sie einen Layer zuweisen können. Der Layer,
dem Sie eine Maske zuweisen, zeigt dann nur an den Pixeln seinen Inhalt an, an denen das
entsprechende Pixel in der Maske nicht transparent ist. Die Alphamaske des Masken-Layers
dient also als Schablone für den Layer; alle anderen Farbwerte sind für die Darstellung irrelevant. Allerdings beachtet der Layer die Stärke der Transparenz, und Sie können somit über
die Maske auch den Hintergrund durch den Layer durchscheinen lassen.
Im Beispielprojekt Layer können Sie eine Ellipse als Maske für alle Layer im Layer des Views
festlegen. Der Viewcontroller erzeugt diese Maske über einen entsprechenden Shapelayer.
Wie Sie in Abbildung 6.20 sehen, funktioniert das nicht nur bei den Layern, die der Viewcontroller explizit angelegt hat, sondern auch bei denen, die zu UIKit-Elementen gehören. Aus
diesem Grund zeigt die App Teile des vorletzten Elements, des Sliders, nicht an. Sie können
also mit Hilfe von Masken auch die Views in Ihren Apps verändern.
Für die Beschreibung der Masken können Sie beliebige Layer-Klassen verwenden. Beispielsweise können Sie durch die Kombination eines Gradienten-Layers mit einem Textlayer als
Maske Farbverläufe in den Buchstaben des Textes erzeugen. Abbildung 6.21 zeigt diesen
Effekt anhand der Kombination der ersten beiden Layer aus Abbildung 6.18, wobei allerdings
der Textlayer keinen weißen Hintergrund besitzt.
653
6
Models, Layer, Animationen
6.3
Core Animation
Der Layer für den Hintergrund erhält die z-Position -1, damit er hinter den anderen Elementen des Buttons liegt. Über die z-Position legen Sie die Lage der Layer zueinander fest. Wenn
zwei Layer sich überlappen, dann überdeckt der Layer mit der höheren z-Position den Layer
mit der niedrigeren.
Da der Button einen transparenten Hintergrund hat, verdeckt er indes nicht den Hintergrund-Layer. Wenn Sie den Button drücken, verändert sich die Farbe des Verlaufs wie beim
Redo-Button in Abbildung 6.22. Dazu besitzt die Klasse zwei Getter, die jeweils ein Array mit
Farben liefern. Die Arrays müssen allerdings die Farbwerte als CGColorRef-Werte enthalten:
Abbildung 6.20 Verschiedene Layer mit Masken
- (NSArray *)normalColors {
return @[(id)[UIColor colorWithRed:0.4 green:0.4 blue:1.0 alpha:1.0].CGColor,
(id)[UIColor colorWithRed:0.0 green:0.0 blue:0.6 alpha:1.0].CGColor,
(id)[UIColor colorWithRed:0.0 green:0.0 blue:0.8 alpha:1.0].CGColor];
}
Abbildung 6.21 Gradienten-Layer mit Textlayer als Maske
- (NSArray *)highligthedColors {
return @[(id)[UIColor colorWithRed:1.0 green:0.4 blue:0.4 alpha:1.0].CGColor,
(id)[UIColor colorWithRed:0.6 green:0.0 blue:0.0 alpha:1.0].CGColor,
(id)[UIColor colorWithRed:0.8 green:0.0 blue:0.0 alpha:1.0].CGColor];
}
Listing 6.47 Definition der Verlaufsfarben für den Hintergrund
6.3.4 Unser Button soll schöner werden
Die Klasse GradientButton der Games-App verwendet einen Gradienten-Layer, um optisch
ansprechende Buttons darzustellen. Dazu fügt er in seiner Methode awakeFromNib einen weiteren Sublayer hinzu:
- (void)awakeFromNib {
[super awakeFromNib];
CALayer *theLayer = self.layer;
CAGradientLayer *theBackground = [CAGradientLayer layer];
theLayer.cornerRadius = 10.0;
theLayer.masksToBounds = YES;
theBackground.frame = theLayer.bounds;
theBackground.startPoint = CGPointMake(0.5, 0.2);
theBackground.endPoint = CGPointMake(0.5, 0.9);
theBackground.colors = self.normalColors;
theBackground.zPosition = –1;
[theLayer addSublayer:theBackground];
self.backgroundLayer = theBackground;
}
Abbildung 6.22 Änderung des Hintergrundverlaufs bei gedrückten Buttons
Damit der Button die Farbe beim Drücken verändert, überschreiben Sie die Methode
setHighlighted:, die Cocoa Touch bei dieser Zustandsänderung aufruft.
- (void)setHighlighted:(BOOL)inHighlighted {
super.highlighted = inHighlighted;
if(inHighlighted) {
self.backgroundLayer.colors = self.highligthedColors;
}
else {
self.backgroundLayer.colors = self.normalColors;
}
}
Listing 6.48 Änderung des Farbverlaufs beim Drücken des Buttons
Listing 6.46 Hinzufügen eines Layers für den Hintergrund
654
655
6
Models, Layer, Animationen
6.3
Core Animation
Das sind auch schon alle Methoden, die der Gradienten-Button benötigt. Wenn Sie einen solchen Button im Interface-Builder anlegen möchten, fügen Sie dafür ein Objekt der Klasse
UIButton ein, dem Sie im Attributinspektor den Typ Custom und Weiß als Textfarbe zuordnen; über den Identitätsinspektor weisen Sie ihm außerdem die Klasse GradientButton zu.
Damit das Bild auf einem Retina-Display nicht unscharf wirkt, erzeugt die Methode den Grafikkontext mit der gleichen Pixelskalierung wie der Bildschirm, wozu sie die Property scale
des Hauptbildschirms ausliest. Diesen Wert können Sie für den dritten Parameter der Funktion UIGraphicsBeginImageContextWithOptions verwenden, um Bilder in der entsprechenden
Auflösung zu erzeugen.
6.3.5 Spieglein, Spieglein an der Wand
- (UIImage *)mirroredImageWithScale:(CGFloat)inScale {
CALayer *theLayer = self.layer;
CALayer *thePresentationLayer = [theLayer presentationLayer];
CGRect theFrame = self.frame;
CGSize theSize = theFrame.size;
UIScreen *theScreen = [UIScreen mainScreen];
CGContextRef theContext;
UIImage *theImage;
Sie können auch auf den Layer-Inhalt zugreifen, beispielsweise um Screenshots zu erzeugen.
Aber auch andere Möglichkeiten, wie beispielsweise Spiegelungen oder Vergrößerungen
(Lupeneffekt), können Sie damit realisieren. Die beiden Spiele verwenden einen View der
Klasse NumberView, um ihre Spielstände anzuzeigen. In diesem View liegen jeweils drei Views
der Klasse DigitView, die jeweils die einzelnen Ziffern darstellen. Der Numberview erzeugt
auch eine gespiegelte, abgeschwächte Kopie der Ziffern, so dass der Eindruck einer glänzenden Oberfläche entsteht.
if(thePresentationLayer) {
theLayer = thePresentationLayer;
}
if([theScreen respondsToSelector:@selector(scale)]) {
CGFloat theScreenScale = [theScreen scale];
Abbildung 6.23 Beispiel für eine Spiegelung
UIGraphicsBeginImageContextWithOptions( theSize, NO, theScreenScale);
}
else {
UIGraphicsBeginImageContext(theSize);
}
theContext = UIGraphicsGetCurrentContext();
CGContextScaleCTM(theContext, 1.0, -inScale);
CGContextTranslateCTM(theContext, 0.0, -theSize.height);
[theLayer renderInContext:theContext];
theImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return theImage;
Für einen Spiegelungseffekt müssen Sie eine vertikal gespiegelte und gestauchte Kopie des
Originalbildes erstellen. Außerdem muss sich das gespiegelte Bild mit zunehmender Entfernung vom Originalbild abschwächen. Ein Beispiel für eine Spiegelung stellt Abbildung 6.23
dar, wo Sie diese drei Effekte – spiegelverkehrte Darstellung, Stauchung und Abschwächung –
sehen.
Tipp
Die Klasse NumberView spiegelt nach diesem Prinzip alle enthaltenen Views. Sie können die
nachfolgend beschriebenen Methoden aus der Kategorie UIView(MirrorImage) auch in eigene
Viewklassen integrieren, um Spiegelungen für beliebige Views zu erhalten.
Die erste Aufgabe, um die Spiegelung zu erzeugen, ist die Bestimmung des Layer-Inhalts. Sie
müssen dazu den Inhalt in ein Bild zeichnen. Die Klasse CALayer stellt für das Zeichnen der
Layer-Hierarchie die Methode renderInContext: zur Verfügung, die den Layer-Inhalt in einen
beliebigen Grafikkontext zeichnet. Das Abbild eines Views wird im Beispiel über die
Methode mirroredImageWithScale: in der Kategorie UIView(MirrorImage) erzeugt. Sie erstellt
ein vertikal gespiegeltes und gestauchtes Abbild eines Views, wobei Sie über den Parameter
den Grad der Stauchung bestimmen können. Wenn Sie hierfür negative Werte verwenden,
schalten Sie die Spiegelung aus. Sie erzeugen also über den Wert –1 ein genaues Abbild des
Views.
656
}
Listing 6.49 Erzeugung eines gespiegelten Abbildes eines Views
Das doppelte Layerchen
Core Animation verwendet für die Darstellung eines Layers zwei weitere Layer. Der Präsentations-Layer enthält den aktuellen Zustand des Layers. In der Regel ist er einfach eine Kopie des
Layers des Views. Während einer Animation unterscheiden sich jedoch die Werte der animierten Propertys des Präsentations-Layers von dem Original, da der Präsentations-Layer den
657
6
Models, Layer, Animationen
aktuellen Animationszustand darstellt. Weil die Spiegelung auch mit Animationen funktionieren soll, verwendet Listing 6.49 den Präsentations-Layer des Layers, sofern er einen besitzt.
Da Sie jetzt ein gestauchtes und gespiegeltes Abbild eines Views erzeugen können, fehlt nur
noch der Abschwächungseffekt, bei dem die Intensität der Farben des Spiegelbildes mit
zunehmender Entfernung vom Originalbild nachlässt. Dieser Effekt lässt sich über einen
grauen Gradientenverlauf und die Funktion CGContextClipToMask erreichen. Die Maske ist
dabei ein Graustufenverlauf mit Grauwerten von 80 % bis 0 %. Der Kontext verwendet
jeweils den Grauwert der Maske als Alphawert für den Pixel, so dass dunklere Grauwerte zu
durchsichtigeren Pixeln führen. Der Verlauf ist ebenfalls ein Bild, das die Methode createGradientImageWithSize:gray: erzeugt; es braucht allerdings nur einen Punkt breit zu sein, da
es CGContextClipToMask automatisch auf die richtige Breite streckt.
- (void)drawMirrorWithScale:(CGFloat)inScale {
CGRect theFrame = self.frame;
CGSize theSize = theFrame.size;
CGPoint thePoint = CGPointMake( CGRectGetMinX(theFrame),
CGRectGetMaxY(theFrame));
CGFloat theHeight = theSize.height * inScale;
CGImageRef theGradient = [self createGradientImageWithSize: CGSizeMake(1.0,
theHeight) gray:0.8];
CGContextRef theContext = UIGraphicsGetCurrentContext();
UIImage *theImage = [self mirroredImageWithScale:inScale];
CGRect theRect;
theRect.origin = thePoint;
theRect.size = theSize;
CGContextSaveGState(theContext);
CGContextClipToMask(theContext, theRect, theGradient);
[theImage drawAtPoint:thePoint];
CGImageRelease(theGradient);
CGContextRestoreGState(theContext);
6.3
Core Animation
- (CGImageRef)createGradientImageWithSize:(CGSize)inSize gray:(float)inGray {
CGImageRef theImage = NULL;
CGColorSpaceRef theColorSpace = CGColorSpaceCreateDeviceGray();
CGContextRef theContext = CGBitmapContextCreate( NULL, inSize.width,
inSize.height, 8, 0, theColorSpace, kCGBitmapByteOrderDefault);
CGFloat theColors[] = {inGray, 1.0, 0.0, 1.0};
CGGradientRef theGradient = CGGradientCreateWithColorComponents( theColorSpace,
theColors, NULL, 2);
CGContextDrawLinearGradient(theContext, theGradient, CGPointZero, CGPointMake(0,
inSize.height), kCGGradientDrawsAfterEndLocation);
CGColorSpaceRelease(theColorSpace);
CGGradientRelease(theGradient);
theImage = CGBitmapContextCreateImage(theContext);
CGContextRelease(theContext);
return theImage;
}
Listing 6.51 Erzeugung des Graustufenverlaufs als Bild
Die Klasse NumberView verwendet nun die Methode drawMirrorWithScale: in ihrer Implementierung von drawRect:, um den Spiegelungseffekt zu erzeugen. Dabei erzeugt sie für jeden
Subview eine eigene Spiegelung (siehe Listing 6.52) mit dem Skalierungsfaktor 0,6.
- (void)drawRect:(CGRect)inRect {
for(UIView *theView in self.subviews) {
[theView drawMirrorWithScale:0.6];
}
}
Listing 6.52 Spiegelung der Subviews
Listing 6.50 Zeichnen der Spiegelung mit einer Graustufenmaske
Abbildung 6.24 zeigt die Spiegelung der Klasse NumberView in der Applikation. Wie wir bereits
erwähnt haben, funktioniert sie auch während der Animation der enthaltenen Layer, so dass
der Effekt sehr realistisch wirkt. Für die Anzeige der Ziffern verwendet der Numberview drei
Views mit der Klasse DigitView, die einen animierten Wechsel ihrer Ziffern erlauben.
Die Methode drawMirrorWithScale: positioniert die Maske und das Spiegelbild an der linken
unteren Ecke des Views. Dabei erzeugt die Methode createGradientImageWithSize:gray:
einen Graustufenverlauf vom angegebenen Wert bis 0 %. Die Implementierung beruht
dabei komplett auf Core Graphics. Der Bitmapkontext in der Methode benutzt einen Graustufenfarbraum, der nur zwei Kanäle (Weiß- und Alphawert) besitzt. Aus diesem Grund muss
das Array theColors mit den Farben für den Verlauf auch nur vier Werte statt acht enthalten.
Abbildung 6.24 Spiegelung der Ziffern des Zugzählers
}
658
659
6
Models, Layer, Animationen
Wie der Pieview besitzt auch der Numberview einen Layer mit einer eigenen Klasse, die allerdings relativ klein ist und keine eigene Header- und Implementierungsdatei besitzt; der
komplette Code befindet sich stattdessen in der Datei DigitView.m. Sie stellt nur die dynamische Property digit zur Verfügung. Listing 6.53 enthält die komplette Deklaration und
Implementierung:
@interface DigitLayer : CALayer
@property (nonatomic) CGFloat digit;
@end
6.3
9
0
1
2
3
4
5
6
@implementation DigitLayer
@dynamic digit;
7
8
9
+ (id)defaultValueForKey:(NSString *)inKey {
return [inKey isEqualToString:kDigitKey] ? @0.0 :
[super defaultValueForKey:inKey];
}
0
9
0
1
2
3
4
5
6
7
8
9
0
9
0
1
2
3
4
5
6
7
8
9
0
Core Animation
9
0
1
2
3
4
5
6
sichtbarer
Bereich
7
8
9
0
Abbildung 6.25 Ziffernwechsel des Digitviews
- (void)drawLayer:(CALayer *)inLayer inContext:(CGContextRef)inContext {
+ (BOOL)needsDisplayForKey:(NSString *)inKey {
return [inKey isEqualToString:kDigitKey] || [super needsDisplayForKey:inKey];
}
CGRect theBounds = self.bounds;
Listing 6.53 Der Layer für die Darstellung der Ziffern
UIFont *theFont = self.font;
CGSize theSize = theBounds.size;
float theDigit = [(DigitLayer *)inLayer digit];
CGSize theFontSize = [@"0" sizeWithFont:theFont];
Obwohl die Ziffern nur ganzzahlige Werte zwischen 0 und 10 annehmen können, speichert
der Layer sie als Fließkommazahl ab. Er soll bei der Animation ja auch Zwischenzustände darstellen können, was sich über ganze Zahlen nicht realisieren lässt. Die Darstellung übernimmt der Digitview, der ja das Delegate des Layers ist. Er zeichnet dazu zwölf Ziffern (9, 0 bis
9 und noch mal die 0) untereinander, wobei er die vertikale Startposition so verschiebt, dass
genau die richtige Ziffer im sichtbaren Bereich des Views zu sehen ist, was er durch ein geeignetes Clipping sicherstellt. Abbildung 6.25 stellt den Wechsel von der Ziffer 5 zur 6 dar.
Der View zeichnet den Layer-Inhalt über die Delegate-Methode drawLayer:inContext:, die
Listing 6.54 enthält. Die Implementierung basiert dabei komplett auf Core Graphics und verwendet größtenteils Funktionen, die Sie bereits in Kapitel 3, »Sehen und anfassen«, kennengelernt haben.
Zur Darstellung der Texte für die Ziffern legt die Methode in Listing 6.54 zunächst den Font
über die Funktion CGContextSelectFont und eine Transformationsmatrix über CGContextSetTextMatrix fest. Da Core Graphics Texte normalerweise vertikal gespiegelt darstellt, gleicht
die Transformationsmatrix im Listing das wieder aus.
660
CGFloat theX = (theSize.width – theFontSize.width) / 2.0;
CGFloat theY = (theFont.capHeight – theSize.height) / 2.0;
theY -= theDigit * theSize.height;
CGContextSaveGState(inContext);
CGContextClipToRect(inContext, theBounds);
CGContextSetFillColorWithColor(inContext, self.backgroundColor.CGColor);
CGContextFillRect(inContext, theBounds);
CGContextSetRGBFillColor(inContext, 0.0, 0.0, 0.0, 1.0);
CGContextSetRGBStrokeColor(inContext, 0.0, 0.0, 0.0, 1.0);
CGContextSelectFont(inContext, [theFont.fontName cStringUsingEncoding:
NSMacOSRomanStringEncoding], theFont.pointSize, kCGEncodingMacRoman);
CGContextSetTextMatrix(inContext, CGAffineTransformMakeScale(1.0, –1.0));
for(int i = 9; i <= 20; ++i) {
char theCharacter = '0' + (i % 10);
661
6
Models, Layer, Animationen
6.3
CGContextShowTextAtPoint(inContext, theX, theY, &theCharacter, 1);
theY += theSize.height;
Core Animation
theHeight, 0.0);
}
}
CGContextRestoreGState(inContext);
[self.superview setNeedsDisplay];
}
Listing 6.54 Zeichnen der Ziffern im Digitview
Die Variablen theX und theY enthalten jeweils die Koordinaten der Ziffern, wobei der x-Wert
fest bleibt und der y-Wert sich in jedem Schleifendurchlauf um die Höhe des Views erhöht.
Die Anfangswerte für diese beiden Variablen sind jeweils die Abstände zum Rand des Views
bei zentrierter Positionierung des Zeichens »0« im View; außerdem zieht die Methode für
jede Ziffer, die vor dem aktuellen Wert liegt, jeweils einmal die View-Höhe vom y-Wert ab.
Die etwas seltsamen Randwerte für die Schleifenvariable i kommen übrigens durch die
eigentliche Zahlenfolge 9, 0, 1, …, 9, 0 zustande, die sich relativ einfach über den Divisionsrest
mit 10 berechnen lässt (siehe Kapitel 3). Sie können das leicht nachprüfen: 9 = 9 % 10, 0 = 10 %
10, 1 = 11 % 10 und so weiter.
Es geht auch einfacher
Mit der Klasse CAReplicationLayer lässt sich der Programmieraufwand für den Spiegelungseffekt reduzieren; allerdings ist damit auch der Effekt nicht ganz so schön wie mit dem bereits
vorgestellten Weg, und der Replication-Layer sollte nur gleich hohe Subviews spiegeln, da der
Layer die gleiche Transformationsmatrix auf alle Sublayer anwendet.
@implementation MirrorView
+ (Class)layerClass {
return [CAReplicatorLayer class];
}
- (void)awakeFromNib {
[super awakeFromNib];
CAReplicatorLayer *theLayer = (CAReplicatorLayer *) self.layer;
UIView *theSubview = [self.subviews objectAtIndex:0];
CATransform3D theTransform = CATransform3DIdentity;
CGFloat theHeight = –0.6 * CGRectGetHeight(theSubview.frame);
CATransform3D theScale = CATransform3DMakeScale(1.0, –0.6, 1.0);
theLayer.instanceAlphaOffset = –0.3;
theLayer.instanceCount = 2;
theLayer.instanceTransform = CATransform3DTranslate(theScale, 0.0,
662
@end
Der Datentyp CATransform3D stellt affine Transformationsmatrizen im dreidimensionalen
Raum dar, deren Verwendung der von CGAffineTransform sehr ähnelt. Die Property instanceCount legt die Anzahl der Kopien fest; in diesem Fall ein Original und eine Kopie für die Spiegelung. Der Layer addiert den Wert der Property instanceAlphaOffset zu dem Alphawert der
Spiegelung. Ein Beispiel für eine solche Spiegelung sehen Sie in Abbildung 6.26.
Abbildung 6.26 Spiegelungseffekt für Arme
Sowohl die Erzeugung als auch die Darstellung einer Spiegelung über Replication-Layer ist also
einfacher als der Weg über die Methode renderInContext:; allerdings ist das Ergebnis auch
nicht ganz so schön.
So, jetzt wird es aber langsam Zeit, dass Sie Ihren Layern Beine machen und sie in Bewegung
setzen.
6.3.6 Der bewegte Layer
Sie können Layer mit Animationen versehen und dadurch ihre Inhalte bewegen. Dadurch
ergeben sich weitere Möglichkeiten für eine ansprechendere Oberflächengestaltung. Dabei
sind Animationen Objekte, deren Klassen die Basisklasse CAAnimation haben, und Sie starten
sie durch das Hinzufügen zum Animationsverzeichnis des Layers, und zwar über die
Methode addAnimation:forKey:.
Wenn Sie eine Animation über eine der beiden Methoden removeAnimationForKey: oder
removeAllAnimations aus dem Verzeichnis entfernen, stoppt sie sofort, und der Layer zeigt
den aktuellen Zustand seiner Werte an. Nehmen wir an, die Property opacity eines Layers hat
den Wert 1 und Sie animieren diesen Wert von 1 nach 0. Wenn Sie die Animation beim Wert
0,5 aus dem Verzeichnis entfernen, springt die Darstellung sofort auf den Wert 1 um. Das
passiert sogar, wenn Core Animation die Animation nach ihrem regulären Ende automatisch entfernt. Der Property-Wert von opacity ist also in diesem Beispiel nach der Animation
immer 1.
Gibt es ein Leben nach der Animation?
Das automatische Entfernen lässt sich über die Property removeOnCompletion der Animation
unterbinden. Wenn sie den Wert NO hat, verbleibt die Animation nach Ablauf im Animations-
663
6
Models, Layer, Animationen
verzeichnis des Layers, der dann auch weiterhin den Endwert der Animation für die Property
verwendet. Im Beispiel hätte der Layer in diesem Fall auch nach Ablauf der Animation den
Wert 0 für opacity; allerdings nur so lange, bis Sie die Animation aus dem Verzeichnis entfernen.
In der Regel ist es besser, die Property vor der Animation auf den gewünschten Endwert zu setzen. Dadurch hat der Layer nach der Animation immer diesen Wert. Eine Animation, die nach
Ihrem Ablauf im Animationsverzeichnis des Layers verbleibt, erinnert an eine gespannte Mausefalle – eine falsche Bewegung, und sie schnappt zu.
Die Property duration der Animation legt die Anzahl der Zeiteinheiten der Animation fest.
Eine Zeiteinheit ist in der Regel eine Sekunde; Sie können die Dauer aber über die Property
speed anpassen.
Bei einer kontinuierlichen Animation setzen Sie über die Property repeatCount die Anzahl
der Wiederholungen. Die Gesamtzeit beträgt in diesem Fall repeatCount * duration Zeiteinheiten. Alternativ können Sie auch über repeatDuration die Gesamtzeit festlegen, und
die Anzahl der Wiederholungen ist repeatDuration / duration. Wenn Sie die Property autoreverses auf YES setzen, ändert die Animation jeweils am Ende ihre Richtung, wodurch sich
außerdem entweder die Gesamtzeit verdoppelt oder die Anzahl der Wiederholungen
halbiert.
Core Animation stellt Ihnen drei grundlegende Animationstypen zur Verfügung:
1. Property-Animationen
6.3
Core Animation
theAnimation.toValue = @0;
theAnimation.duration = 1.0;
[theLayer addAnimation:theAnimation forKey:@"opacity"];
Listing 6.55 Einfache Animation eines Property-Wertes
Abhängig von der Property können dabei die Interpolationswerte Objekte der Klassen
NSNumber (für Ganzzahl- und Fließkommawerte) oder NSValue (für die Strukturen CGPoint,
CGSize, CGRect und CATransform3D) sein.
Eine Animation der Klasse CABasicAnimation interpoliert zwischen zwei Property-Werten. Sie
können dabei über die Propertys fromValue und toValue den Start- beziehungsweise den Endwert der Animation festlegen oder über die Property byValue den Abstand vom Start- oder
Endwert angeben. Dabei sind unterschiedliche Kombinationen beim Setzen dieser Propertys möglich. Die möglichen Kombinationen sind in Tabelle 6.6 dargestellt. Dabei bedeutet
ein »x« in den drei linken Spalten, dass Sie die entsprechende Property im Animationsobjekt
auf einen Wert ungleich nil gesetzt haben, und die Angabe »aktueller Property-Wert«
bezieht sich auf den Präsentations-Layer.
»fromValue«
»byValue«
»toValue«
Animation
von
nach
x
nil
x
fromValue
toValue
x
x
nil
fromValue
fromValue + byValue
nil
x
x
toValue – byValue
toValue
x
nil
nil
fromValue
Aktueller PropertyWert
nil
x
nil
Aktueller Property-Wert
Aktueller PropertyWert plus byValue
nil
nil
x
Aktueller Property-Wert
toValue
2. Transitionen
3. Animationsgruppen
Wir stellen Ihnen diese Typen im Folgenden genauer vor.
Property-Animationen
Dieser Animationstyp verändert den Wert einer Layer-Property während der Ausführung. Er
basiert auf der Klasse CAPropertyAnimation, in deren Objekten Sie den Keypath auf die zu animierende Property angeben müssen. Um diesen Animationstyp anzuwenden, verwenden
Sie die Unterklassen CABasicAnimation oder CAKeyframeAnimation. Alternativ können diese
Animationen auch den Schlüssel aus dem Animationsverzeichnis verwenden, wenn Sie sie
über den Convenience-Konstruktor animation anlegen. Dadurch können Sie mit einem Animationsobjekt mehrere unterschiedliche Property-Werte animieren. Beispielsweise lässt
sich die Property opacity folgendermaßen animieren:
Tabelle 6.6 Animationsbereich in Abhängigkeit von den gesetzten Propertys
Das Setzen des Property-Wertes toValue in Listing 6.55 ist also nicht notwendig, da die Animation auch automatisch diesen Wert als Zielwert verwendet. Das folgende Beispiel verschiebt einen Layer zwischen zwei Punkten. Als Property-Namen verwendet die Animation
den Schlüssel, unter dem sie im Animationsverzeichnis des Layers liegt – also position.
CABasicAnimation *theAnimation = [CABasicAnimation animation];
theLayer.opacity = 0.0;
theAnimation.fromValue = @1;
664
CABasicAnimation *theAnimation = [CABasicAnimation animation];
theAnimation.fromValue = [NSValue valueWithCGPoint:CGPointMake(10.0, 10.0)];
665
6
Models, Layer, Animationen
6.3
theAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(100.0, 10.0)];
[theLayer addAnimation:theAnimation forKey:@"position"];
Listing 6.56 Verschieben eines Layers über eine Animation
Über die Klasse CAKeyframeAnimation können Sie Property-Animationen mit komplexeren
Verläufen erzeugen. Über die Property values übergeben Sie dabei ein Array mit den Werten
für die Zwischenzustände der Animation. Falls Sie mit der Animation Werte vom Typ CGPoint
animieren wollen, können Sie auch die Property path verwenden. Sie erwartet einen CoreGraphics-Pfad. Damit ist es beispielsweise einfach, einen Layer entlang einer Kreislinie zu
bewegen:
CAKeyframeAnimation *theAnimation = [CAKeyframeAnimation animation];
CGMutablePathRef thePath = CGPathCreateMutable();
CGPathAddEllipseInRect(thePath, NULL, CGRectMake(140.0, 150.0, 20.0, 20.0));
theAnimation.path = thePath;
theAnimation.repeatCount = 3;
theAnimation.autoreverses = YES;
theAnimation.duration = 0.5;
[theLayer addAnimation:theAnimation forKey:@"position"];
CGPathRelease(thePath);
Listing 6.57 Layer-Bewegung entlang eines Pfades
Mit Keyframe-Animationen können Sie recht ungewöhnliche Animationen verwirklichen.
Abschnitt 6.3.7, »Daumenkino«, stellt dafür noch ein komplexeres Beispiel vor.
Transitionen
Der zweite Animationstyp sind Transitionen, die Core Animation über die Klasse CATransition abbildet. Diese Klasse unterstützt insgesamt vier unterschiedliche Transitionstypen,
die Tabelle 6.7 auflistet. Außer in der Fade-Animation können Sie eine Richtung in der Property subtype angeben; die Konstanten kCATransitionFromRight, -Left, -Top und -Bottom enthalten die erlaubten Werte hierfür.
Name
Beschreibung
kCATransitionFade
Blendet den bestehenden Layer aus und den neuen Layer ein.
kCATransitionMoveIn
Bewegt den neuen Layer über den bestehenden.
kCATransitionPush
Schiebt den neuen Layer herein und den bestehenden gleichzeitig
hinaus.
Tabelle 6.7 Die Transitionstypen von Core Animation
666
Name
Beschreibung
kCATransitionReveal
Schiebt den bestehenden Layer hinaus und gibt dadurch den
neuen View frei.
Core Animation
Tabelle 6.7 Die Transitionstypen von Core Animation (Forts.)
Sie wenden eine Transition an, indem Sie sie in das Animationsverzeichnis desjenigen Layers einfügen, der die Transitionslayer enthält. Außerdem müssen Sie einen bestehenden
Layer unsichtbar machen, indem Sie ihn verstecken oder aus der Layer-Hierarchie entfernen. Analog müssen Sie den neuen Layer anzeigen oder in die Hierarchie einfügen. Listing 6.58 zeigt ein einfaches Beispiel für diesen Animationstyp.
CATransition *theTransition = [CATransition animation];
theTransition.type = kCATransitionPush;
theTransition.subtype = kCATransitionFromTop;
[[theLayer.sublayers objectAtIndex:0] setHidden:YES];
[[theLayer.sublayers objectAtIndex:1] setHidden:NO];
[theLayer addAnimation: theTransition forKey:@"transition"];
Listing 6.58 Ausführen einer Transition
Die Transitionen in Core Animation sind also den Transitionen des UIKits sehr ähnlich.
Allerdings bietet Letzteres erstaunlicherweise eine größere Vielfalt an Standardübergängen.
Animationsgruppen
Die Klasse CAAnimationGroup fasst mehrere Layer-Animationen zu einer Gruppe zusammen,
die Sie in einem Array an die Property animations übergeben. Die Gruppe gibt die maximale
Dauer der Animationen vor und beendet die Ausführung längerer Animationen gegebenenfalls vorzeitig. Eine Animationsgruppe ist also die Queen unter den Animationen. Wenn sie
aufhört zu essen, dann müssen auch alle anderen den Dessertlöffel fallen lassen. Hat beispielsweise die Gruppe eine Dauer von einer Sekunde und enthält sie eine 4 Sekunden dauernde Animation, dann wird diese Animation nur zu einem Viertel ausgeführt. In Listing 6.59
führt diese Konfiguration beispielsweise dazu, dass die Animation des Eckradius nur von
0 bis 40 anstatt bis 160 läuft.
CABasicAnimation *theAnimation = [CABasicAnimation animationWithKeyPath:
@"cornerRadius"];
CAAnimationGroup *theGroup = [CAAnimationGroup animation];
theAnimation.toValue = @160.0;
theAnimation.duration = 4.0;
theGroup.duration = 1.0;
667
6
Models, Layer, Animationen
6.3
theGroup.animations = @[theAnimation];
theGroup.repeatCount = 3.0;
[theLayer addAnimation:theGroup forKey:@"group"];
}
Listing 6.59 Erzeugung einer Animationsgruppe
Listing 6.60 Laden der Bildfolge für die Animation
Core Animation
CFRelease(theSource);
return theResult;
6.3.7 Daumenkino
Core Graphics und Objective-C
Nachdem wir in Abschnitt 6.3.5, »Spieglein, Spieglein an der Wand«, die verschiedenen Animationsarten kurz vorgestellt haben, soll jetzt ein etwas umfangreicheres Beispiel für eine
Keyframe-Animation folgen. In Listing 6.57 haben Sie bereits ein Beispiel für eine Bewegung
entlang eines Pfades kennengelernt. Es ist jedoch auch möglich, mit einer Keyframe-Animation die Objekte einer Property des Layers auszutauschen. Sie können diese Möglichkeit beispielsweise dafür nutzen, Bildfolgen ablaufen zu lassen und somit einfache Filme wie bei
einem Daumenkino abzuspielen.
Core Foundation enthält viele Datentypen, deren zugehörige Funktionen sich analog zu den
Methoden einer entsprechenden Foundation-Klasse verhalten.
Projektinformation
Den Quellcode des Beispielprojekts FlipBookAnimation finden Sie im Github-Repository zum
Buch im Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Ausgabe_iOS8/
Apps/iOS5/FlipBookAnimation.
Das Beispielprojekt FlipBookAnimation lädt eine Bildfolge aus einem GIF-Bild und stellt sie
über eine Keyframe-Animation dar. Dabei öffnet und lädt die Applikation die Bildfolge
über das ImageIO-Framework in der Methode readAnimationImages der Klasse FlipBookViewController, die Sie in Listing 6.60 sehen.
- (NSArray *)readAnimationImages {
NSBundle *theBundle = [NSBundle mainBundle];
NSURL *theURL = [theBundle URLForResource: @"animated-image" withExtension:
@"gif"];
NSDictionary *theOptions = [NSDictionary dictionaryWithObjectsAndKeys:nil];
CGImageSourceRef theSource = CGImageSourceCreateWithURL( (__bridge
CFURLRef)theURL, (__bridge CFDictionaryRef)theOptions);
size_t theCount = CGImageSourceGetCount(theSource);
NSMutableArray *theResult = [NSMutableArray arrayWithCapacity:theCount];
for(size_t i = 0; i < theCount; ++i) {
CGImageRef theImage = CGImageSourceCreateImageAtIndex( theSource, i,
(__bridge CFDictionaryRef)theOptions);
[theResult addObject:(__bridge_transfer id)theImage];
}
Ein Beispiel dafür sind die Datentypen CFURLRef und CFDictionaryRef mit den analogen Klassen NSURL und NSDictionary.
Tatsächlich implementiert das Foundation-Framework die Klasse NSURL über den Datentyp
CFURLRef, so dass Sie NSURL-Objekte auf CFURLRef und umgekehrt casten können, wie das die
vierte Anweisung in Listing 6.60 macht. Diese Technologie nennt Apple Toll-free Bridging.
Wenn Sie allerdings Foundation- auf Core-Foundation-Referenzen in einer ARC-Umgebung
casten, müssen Sie die Modifizierer __bridge, __bridge_retained oder __bridge_transfer
verwenden, damit der ARC-Compiler weiß, wie er mit der Eigentümerschaft auf den Referenzen umgehen soll.
Wenn Sie den Modifizierer weglassen, warnt Xcode Sie an der betreffenden Stelle und macht
einen Vorschlag, wie Sie die Warnung vermeiden können. In der Regel sollten Sie diesen Vorschlag befolgen. Abschnitt 6.5, »Einführung in das Sprite-Kit«, geht noch genauer auf die Tollfree Bridge und die Modifizierer ein.
Die Methode öffnet über die URL das GIF-Bild und liefert eine Referenz des Typs CGImageSourceRef darauf. Danach ermittelt sie die Anzahl der Einzelbilder und schreibt diese Zahl in
die Variable theCount. Über eine Schleife liest die Methode die einzelnen Bilder aus der
Imagesource in ein Array. Die gelesenen Bilder sind allerdings keine Objekte mit der Klasse
UIImage sondern haben den Typ CGImageRef, was sehr praktisch ist, da ja die contents-Property des Layers diesen Typ erwartet.
Die Methode viewDidAppear: des Viewcontrollers erzeugt und startet die Animation. Dazu
verwendet sie eine Keyframe-Animation und übergibt das Array mit den Bildern an die Property values der Animation. Die Dauer eines kompletten Durchlaufs ist eine Sekunde, und die
Property repeatCount bekommt den Wert HUGE_VALF zugewiesen, so dass die Animation
unbegrenzt läuft.
- (void)viewDidAppear:(BOOL)inAnimated {
[super viewDidAppear:inAnimated];
NSArray *theImages = [self readAnimationImages];
CALayer *theLayer = self.animationView.layer;
CAKeyframeAnimation *theAnimation = [CAKeyframeAnimation animation];
theAnimation.values = theImages;
668
669
6
Models, Layer, Animationen
6.3
Core Animation
theAnimation.repeatCount = HUGE_VALF;
theAnimation.duration = 1.0;
[theLayer addAnimation:theAnimation forKey:@"contents"];
Allerdings deklariert nicht die Klasse CAAnimation diese Propertys, sondern das Protokoll
CAMediaTiming. Der Versuch, die Eigenschaften einer laufenden Animation zu ändern, scheitert jedoch mit folgender Ausnahme:
Listing 6.61 Ausführen der Bildfolge über eine Keyframe-Animation
Terminating app due to uncaught exception 'CAAnimationImmutable', reason: 'attempting
to modify read-only animation <CAKeyframeAnimation: 0x7546810>'.
}
Die Animation stellt einen Atomkern dar, um den drei Elektronen in unterschiedlichen Bahnen kreisen. Wenn Sie die Animation langsamer laufen lassen, beispielsweise mit einer
Dauer von fünf Sekunden, dann flackern die Elektronen bei der Bewegung, und Sie sehen für
jedes Elektron jeweils zwei blasse Punkte (siehe Abbildung 6.27). Das liegt daran, dass die Animation die Bilder nicht wechselt, sondern aus- beziehungsweise einblendet. Diese Überblendzeiten sind dabei so lang, dass Sie die Punkte doppelt sehen. Das Beispielprojekt zeigt
außerdem einen Pause-Button an, mit dem Sie die Animation unterbrechen können. Wenn
Sie ihn drücken, können Sie sich diesen Dopplungseffekt in Ruhe ansehen.
Über die Property calculationMode der Klasse CAKeyframeAnimation können Sie das Überblendverhalten ändern. Sie hat standardmäßig den Wert kCAAnimationLinear, blendet also
das neue Bild in der gleichen, gleichmäßigen Geschwindigkeit ein, wie sie das bestehende
ausblendet. Wenn Sie stattdessen den Wert auf kCAAnimationDiscrete setzen, tauscht die Animation die Bilder ohne Überblendung aus. Dadurch sehen Sie jedes Elektron sowohl während der Bewegung als auch in den Pausen nur einmal, wenn Sie die Anweisung
theAnimation.calculationMode = kCAAnimationDiscrete;
an geeigneter Stelle in die Methode viewDidAppear: einfügen.
Sie können also beispielsweise eine Animation nicht einfach über ihre Property speed verlangsamen oder gar pausieren lassen.
Allerdings implementiert auch die Klasse CALayer das Protokoll, und so können Sie die Eigenschaften auch bei den Layern setzen und damit den Zeitraum beziehungsweise die Zeitachse
der enthaltenen Animationen verbiegen. Wenn Sie beispielsweise die Geschwindigkeit des
Layers auf 2 setzen, laufen seine Animationen doppelt so schnell ab, und wenn Sie sie auf 0
setzen, hält die Animation an.
Geschachtelte Zeiträume
Jeder Layer legt also seine eigene Zeitachse fest, die allerdings nicht nur für die enthaltenen
Animationen, sondern auch für alle enthaltenen Layer gilt. Der innere Layer liegt also auch
immer im Zeitraum seines Superlayers. Wenn der äußere Layer beispielsweise die Geschwindigkeit 3, der innere die halbe Geschwindigkeit und eine Animation des inneren Layers den
Wert 4 hat, dann ist die Gesamtgeschwindigkeit 3 × 0,5 × 4 = 6.
Sie können also über den Layer eine Animation pausieren lassen. Um diese Funktionalität
unabhängig von der Layer-Klasse zu machen, verwendet das Beispielprojekt dafür die Kategorie CALayer(AnimationPausing) mit den Methoden pause, resume und isPausing. Wenn Sie
nun allerdings diese drei Methoden folgendermaßen implementieren,
- (void)pause {
self.speed = 0.0;
}
- (void)resume {
self.speed = 1.0;
}
- (BOOL)isPausing {
return self.speed == 0.0;
}
Abbildung 6.27 Die Animation macht mal eine Pause.
6.3.8 Relativitätstheorie
In Abschnitt 6.3.6, »Der bewegte Layer«, haben Sie die Propertys duration, speed, repeatCount
und autoreverses kennengelernt, mit denen Sie das Ablaufverhalten der Animation steuern.
670
Listing 6.62 Naive Implementierung der Pause-Funktionalität
stellen Sie zwei unerwünschte Effekte fest: Einerseits verschwindet der Layer-Inhalt, andererseits scheint die Animation im Hintergrund weiterzulaufen. Denn nach dem Ende der
Pause befinden sich die Elektronen meistens nicht an der gleichen Position wie zu Beginn
der Pause.
671
6
Models, Layer, Animationen
6.3
Vermutlich zeigt der Layer immer das Bild an, das er vor dem Beginn der Animation hatte,
und das war keins, weil die App der contents-Property keinen Wert zugewiesen hat. Sie können das leicht überprüfen, indem Sie diesen Wert durch die Anweisung theLayer.contents =
[theImages objectAtIndex:0]; in der Methode viewWillAppear: setzen. Durch diese Änderung zeigt nun der Layer immer das gleiche Bild in der Pause an.
Diese Änderung löst zwar nicht das Problem, jedoch wissen Sie nun, was die Ursache des Fehlers ist. Durch die Änderung der Geschwindigkeit fällt die Zeitachse des Layers auf einen
Punkt – den Startzeitpunkt – zusammen, und somit bleibt auch die Animation am Startzeitpunkt stehen.
Sie müssen also die Animation so anhalten, dass sie stattdessen am aktuellen Zeitpunkt hält.
Dazu liefert Ihnen die Funktion CACurrentMediaTime die aktuelle Systemzeit für die Animationen, die Sie über die Methode convertTime:fromLayer: in die aktuelle Zeit des Layers
umrechnen können. Da Sie hier mit der absoluten Zeit rechnen, geben Sie für den zweiten
Parameter den Wert nil an.
Den so errechneten Wert setzen Sie über die Property timeOffset des Layers. Sie enthält eine
Zeitspanne, die Core Animation zu der aktuellen Zeit der Animation hinzuzählt; diese Property verschiebt also den aktuellen Punkt in der Animation. Im Gegensatz dazu legen Sie über
die Property beginTime die Startzeit fest.
Sie können sich das so vorstellen: Sie stehen in Ihrer Küche und möchten plötzlich ein
bestimmtes Musikstück hören. Also gehen Sie ins Wohnzimmer, suchen die Schallplatte heraus, legen sie auf den Plattenspieler und setzen die Nadel genau auf den Beginn des zweiten
Stückes auf der Platte. Dann ist die Zeit vom Start in der Küche bis zum Aufsetzen der Nadel
die Begintime, und die Dauer des ersten Stückes auf der Platte ist der Timeoffset, wenn die
gesamte Musik auf der Platte der Animation entspricht.
Abbildung 6.28 stellt diesen Zusammenhang grafisch dar. Die Animation startet an dem Zeitpunkt, auf den die Spitze des Pfeils beginTime zeigt. Allerdings startet sie nicht am Anfang,
sondern um den Timeoffset nach links verschoben. Der Animationsablauf entspricht also
der dicken Linie zwischen den schwarzen Punkten.
Core Animation
-(void)pause {
CFTimeInterval theCurrentTime = CACurrentMediaTime();
CFTimeInterval theTime = [self convertTime:theCurrentTime fromLayer:nil];
self.speed = 0.0;
self.timeOffset = theTime;
}
Listing 6.63 Anhalten zum richtigen Animationszeitpunkt …
Mit der Änderung aus Listing 6.63 zeigt der Layer immer schön den Animationszustand an,
an dem Sie Pause gedrückt haben. Allerdings lässt das Fortsetzen der Animation noch zu
wünschen übrig, da hierbei unschöne Elektronensprünge auftreten, worüber sich allenfalls
Quantenphysiker freuen. Sie müssen also beim Wiederanlaufen dafür sorgen, dass der Animationszustand genau dem Zustand in der Pause entspricht. Diese Berechnung ist etwas
komplizierter. Abbildung 6.29 stellt das Vorgehen dafür zunächst grafisch dar.
pause
abgelaufener
Animationsteil
abgelaufener
Animationsteil
resume
Animation
Zeitachse
Animation
Zeitachse
Abbildung 6.29 Zeitverschiebung beim Fortsetzen der Animation
Die Darstellung enthält zwei Zeitverläufe. Die obere Zeitachse enthält den realen Animationsverlauf: Die Animation startet und läuft eine gewisse Zeit. Dann halten Sie sie an, bis Sie
über resume die Bewegung fortsetzen. Wenn Sie das Zeitstück der Pause nun mit dem abgelaufenen Animationsteil vertauschen, erhalten Sie die untere Zeitachse aus der Abbildung,
beginTime
duration
Animation
Zeitachse
und die beiden Animationsteile knüpfen nahtlos aneinander an. Auf diese Weise lässt sich
also der Sprung vermeiden.
Im Code verschieben Sie das Zeitstück der Pause, indem Sie es als Begintime der Animation
timeOffset
setzen (siehe Abbildung 6.28), und die Länge der Pause entspricht der Differenz aus der aktuellen Zeit und der Startzeit der Pause.
Abbildung 6.28 Startzeitpunkt auf der Zeitachse
Listing 6.64 enthält den kompletten Code für die resume-Methode. Da die Methode convert-
Der Timeoffset bietet also genau die Möglichkeit, die richtige Position im Animationsablauf
auszuwählen, und Sie ändern die Methode pause wie folgt:
es übrigens wichtig, diesen Wert vorher auf null zu setzen.
Time:fromLayer: für ihre Berechnungen auch den Wert der Property beginTime verwendet, ist
672
673
6
Models, Layer, Animationen
-(void)resume {
CFTimeInterval theTime = self.timeOffset;
6.3
Core Animation
Mit den Methoden aus Abschnitt 6.2.1, »Animationen mit Blöcken«, ist es wesentlich einfacher, eine Animation zu erzeugen. Natürlich verwendet Cocoa Touch auch dafür Layer-Animationen, die der Layer bei Änderungen der entsprechenden Property automatisch erzeugt.
self.speed = 1.0;
Dafür wäre es naheliegend, die Erzeugung der Animation in den Setter der Property des
self.timeOffset = 0.0;
Views oder des Layers zu verschieben. Allerdings würden Sie damit die Animationserzeu-
self.beginTime = 0.0;
gung fest mit dem View oder dem Layer verdrahten.
theTime = [self convertTime:CACurrentMediaTime() fromLayer:nil] – theTime;
Core Animation stellt stattdessen für die Animationserzeugung einen eleganteren Weg zur
self.beginTime = theTime;
Verfügung. Ein Layer kann bei der Änderung einer Property oder der Veränderung der Layer-
}
Hierarchie eine Aktion auslösen. Eine Aktion ist dabei ein Objekt, dessen Klasse das Protokoll
Listing 6.64 … und Fortfahren mit dem richtigen Animationszustand
CAAction implementiert. Die Property-Änderung besteht dabei aus drei Schritten:
1. Zuerst übernimmt der Layer den neuen Wert.
6.3.9 Der View, der Layer, seine Animation und ihr Liebhaber
2. Als Nächstes erzeugt er die Aktion.
Mit einer Property-Animation können Sie allerdings nicht nur die Standard-Propertys der
3. Danach führt der Layer die Aktion aus.
Layer, sondern auch beliebige eigene Propertys animieren. Dazu sollten Sie diese Propertys
Die Layer-Methode actionForKey: erzeugt die Aktionen des Layers. Wenn Sie die Standard-
analog zu den Propertys part und digit in den Klassen PieLayer beziehungsweise DigitView
aktionen eines Layers verändern oder neue Aktionen hinzufügen wollen, haben Sie mehrere
anlegen, wie das Abschnitt 6.3.1, »Layer«, beschreibt. Hier sind noch einmal kurz die Punkte,
Möglichkeiten:
auf die Sie bei animierbaren Propertys achten sollten:
1. Deklarieren Sie die Property in Ihrem Layer, und implementieren Sie sie über das Schlüsselwort @dynamic. Core Animation durchsucht nur die Propertys, jedoch keine Methoden
der Layer-Klasse nach animierbaren Propertys.
2. Legen Sie über die Klassenmethode defaultValueForKey: einen Initialwert für Ihre Property fest.
3. Außerdem müssen Sie die Klassenmethode needsDisplayForKey: überschreiben, damit
Core Animation den Layer bei einer Änderung des Property-Wertes auch zeichnet.
Sie können also beispielsweise eine Animation der Klasse CABasicAnimation mit dem Schlüs-
1. Implementieren Sie die Methode actionForLayer:forKey: im Delegate des Layers.
2. Setzen Sie über die Property actions im Layer ein Verzeichnis mit Aktionen.
3. Der Layer durchsucht außerdem sein Style-Verzeichnis rekursiv nach Aktionen.
4. Implementieren Sie die Klassenmethode defaultActionForKey:.
Die Methode actionForKey: sucht in der angegebenen Reihenfolge nach den Aktionen. Wenn
ein Suchweg dabei nil liefert, sucht der Layer weiter. Ein Aktionsobjekt oder das Nullobjekt
[NSNull null] bricht die Suche sofort ab. Dabei bedeutet die Rückgabe des Nullobjekts, dass
der Layer keine Aktion zum angegebenen Schlüssel hat.
sel part in das Animationsverzeichnis eines Pie-Layers einfügen. Core Animation erzeugt
Das Protokoll CAAction besitzt nur eine Methode runActionForKey:object:arguments:, die der
dann während der Animation die entsprechenden Zwischenwerte für diese Property, so dass
Layer im dritten Schritt aufruft. Dabei ist der Parameter object der Layer, der die Aktion aus-
ein flüssiger Bewegungsablauf entsteht.
gelöst hat, und der Parameter arguments ist in der Regel nil. Die Klasse CAAnimation implementiert das CAAction-Protokoll. Die Implementierung der Methode fügt dabei lediglich das
Bist du flüssig?
Wie Sie vielleicht schon in Abschnitt 6.3.7, »Daumenkino«, bemerkt haben, sind die Animationen in Core Animation zeit- und nicht framebasiert. Das heißt, dass die von Ihnen vorgegebene Zeit für die Animation wichtiger ist als die Anzahl der erzeugten Einzelbilder (Frames).
Core Animation berechnet, sofern das möglich ist, für die Animation so viele Einzelbilder, dass
die Animation die vorgegebene Zeit nicht überschreitet und dabei trotzdem möglichst flüssig
abläuft. Dahinter steckt sicherlich jede Menge Hirnschmalz. Wenn Sie also für Ihre Views und
Layer Animationen brauchen, sollten Sie dafür Core Animation verwenden.
674
Animationsobjekt in das Animationsverzeichnis des Layers ein. Die Implementierung sieht
dafür ungefähr so aus:
- (void)runActionForKey:(NSString *)inKey object:(id)inLayer arguments:(NSDictionary
*)inArguments {
[(CALayer *)inLayer addAnimation:self forKey:inKey];
}
Listing 6.65 Action-Methode einer Animation
675
6
Models, Layer, Animationen
Wenn Sie also eigene Layer-Propertys automatisch animieren möchten, brauchen Sie nur
über einen der oben beschriebenen Suchwege ein Animationsobjekt zu erzeugen. Alternativ
überschreiben Sie in Ihrer Layer-Klasse die Methode actionForKey:. Eine Implementierung
der Delegate-Methode sieht beispielsweise so aus:
- (id<CAAction>)actionForLayer:(CALayer *)inLayer forKey:(NSString *)inKey {
if([kPartKey isEqualToString:inKey]) {
CABasicAnimation *theAnimation = [CABasicAnimation
animationForKeyPath:inKey];
theAnimation.fromValue = [inLayer valueForKey:kPartKey];
return theAnimation;
}
else {
return [super actionForLayer:inLayer forKey:inKey];
}
}
Listing 6.66 Animationserzeugung für eine Layer-Property
Mit der Implementierung aus Listing 6.66 führt jede Änderung der Property part der Klasse
PieView in einem Animationsblock zu einer animierten Veränderung der Grafik. Da der Layer
die Aktion vor der Übernahme des Property-Wertes anlegt, liefert der Aufruf von valueForKey: noch den alten Wert. Weil der Layer dagegen den Wert vor der Ausführung der Aktion
übernimmt, reicht es nach Tabelle 6.6 aus, die Property fromValue zu setzen.
Das klingt ja schon mal ganz gut, hat jedoch noch einen kleinen Schönheitsfehler: Wenn Sie
die Dauer des Animationsblocks verändern, ist das der Animation für die Property part völlig schnuppe. Leider hat Apple den Zugriff auf diesen und die anderen Animationsparameter
sehr gut versteckt, so dass Sie diese Werte nicht einfach auslesen können.
Die Lösung dieses Problems ist allerdings einfach und erfolgt nach dem Motto: »Klauen wir
gleich die ganze Animation.« Weniger cineastisch ausgedrückt heißt das, dass die Methode
also nicht die Animation selbst erzeugt, sondern dass sie sie für eine Standard-Property, in
diesem Fall opacity, erzeugen lässt und sie danach anpasst.
if([kPartKey isEqualToString:inKey]) {
CABasicAnimation *theAnimation = (id)[inLayer actionForKey:@"opacity"];
theAnimation.keyPath = inKey;
theAnimation.fromValue = [inLayer valueForKey:kPartKey];
theAnimation.toValue = nil;
theAnimation.byValue = nil;
}
Listing 6.67 Erzeugung der Animation über eine Standard-Property
676
6.3
Core Animation
Listing 6.67 gibt den geänderten if-Block gegenüber Listing 6.66 wieder, der die Animation
über die Animation für die Property opacity erzeugt. Sie müssen in dieser Animation natürlich die Werte der Propertys keyPath und fromValue anpassen. Da Vorsicht bekanntlich die
Mutter der Porzellankiste ist, sollten Sie die Property-Werte für toValue und byValue löschen.
Sie können damit jetzt auch die Property part der Klasse PieView in einem Animationsblock
setzen, um die Änderung zu animieren:
[UIView animateWithDuration:0.75 animations:^{
self.pieView.part = 0.6;
}];
Listing 6.68 Animation einer selbstdefinierten Property
Die Klasse NumberView im Beispielprojekt Games animiert die drei Anzeigen für die Ziffern
auch über View-Animationen. Sie besitzt zwei Methoden, um einen neuen Wert zu setzen.
Da ist einerseits der Setter setValue:, der nur den Wert setzt, und andererseits setValue:
animated:, der außerdem eine animierte Aktualisierung der Anzeige erlaubt. Dabei basiert
die zweite Methode auf der Implementierung der ersten; abhängig vom zweiten Parameter
kapselt sie die Zuweisung einfach in einen Animationsblock und ähnelt damit in der Signatur vielen Methoden des UIKits:
- (void)setValue:(NSUInteger)inValue animated:(BOOL)inAnimated {
if(inAnimated) {
[UIView animateWithDuration:0.75 animations:^{
self.value = inValue;
}];
}
else {
self.value = inValue;
}
}
Listing 6.69 Aktualisierung des Spielstands mit und ohne Animation
Der Setter teilt den Wert für die einzelnen Ziffern der enthaltenen Digitviews auf und aktualisiert deren Werte. Dabei liegen diese Views allerdings in umgekehrter Reihenfolge im Numberview. Der erste Subview liegt also am weitesten rechts und enthält die Einerstelle, der
zweite Subview liegt links daneben und enthält die Zehner und so weiter. Die Berechnung
der Ziffern erfolgt dabei über eine Division durch 10 mit Rest.
- (void)setValue:(NSUInteger)inValue {
NSUInteger theValue = inValue;
for(DigitView *theView in self.subviews) {
677
6
Models, Layer, Animationen
6.3
theView.digit = value % 10;
theValue /= 10;
return theAnimation;
}
else {
return [super actionForLayer:inLayer forKey:inKey];
}
}
value = inValue;
}
Listing 6.70 Aktualisierung der Ziffern
Core Animation
}
Listing 6.72 Erzeugung der Animation für den Digitview
Mit einer entsprechend adaptierten Version der oben beschriebenen Methode actionForLayer:forKey: erhalten Sie einen animierten Wechsel der Ziffern. Allerdings läuft der View
alle Ziffern bei einem Wechsel von 9 auf 0 durch, der beispielsweise beim Übergang von der
9 auf die 10 auftritt. Um diesen Darstellungsfehler zu vermeiden, berechnet der Digitview im
Setter der Property value einen besseren Wert für die Property fromValue der Animation. Der
Digitview speichert diesen Wert in einer eigenen Property und übergibt ihn an die Animation. Der Wert ist 10 für den Übergang von 0 auf 9 und –1 für den Übergang von 9 auf 0.
- (void)setDigit:(NSUInteger)inDigit {
NSInteger theOldDigit = self.digit;
NSInteger theNewDigit = inDigit % 10;
if(theOldDigit == 9 && theNewDigit == 0) {
theOldDigit = –1;
}
else if(theOldDigit == 0 && theNewDigit == 9) {
theOldDigit = 10;
}
self.fromValue = @(theOldDigit);
[(DigitLayer *)self.layer setDigit:theNewDigit];
}
Listing 6.71 Berechnung eines geeigneteren Startwertes für die Animation
Diesen berechneten Startwert in fromValue übergibt die Methode actionForLayer:forKey: an
die Property fromValue der Animation. Listing 6.72 zeigt die komplette Implementierung und
hebt die angepasste Wertübergabe hervor.
- (id<CAAction>)actionForLayer:(CALayer *)inLayer forKey:(NSString *)inKey {
if([kDigitKey isEqualToString:inKey]) {
CABasicAnimation *theAnimation = (id)[inLayer actionForKey:@"opacity"];
theAnimation.keyPath = inKey;
theAnimation.fromValue = self.fromValue;
theAnimation.toValue = nil;
theAnimation.byValue = nil;
678
6.3.10 Transaktionen
Die Methoden zur Aktionserzeugung des vorangegangenen Abschnitts bedienten sich eines
kleinen, vielleicht nicht ganz sauberen Tricks, um die Animation von außen parametrisieren
zu können. Das ist leider notwendig, wenn Sie die Animation über die bekannten Animationsmethoden der Klasse UIView erzeugen wollen. Core Animation bietet hingegen über
sein Transaktionskonzept einen eigenen Weg, Animationen zu konfigurieren.
Jede Änderung des Layer-Baums, wie beispielsweise das Hinzufügen, Umordnen oder Entfernen von Layern oder das Ändern von Layer-Propertys, erfolgt in einer Core-AnimationTransaktion. Bislang haben Sie nur implizite Transaktionen verwendet: Wenn beispielsweise
das Programm die Property part des Pie-Layers auf 0,5 gesetzt hat, hat Core Animation eine
Transaktion gestartet, den neuen Wert übernommen und die Transaktion beendet.
Sie können jedoch über die Klassenmethoden begin und commit der Klasse CATransaction
auch explizite Transaktionen starten und beenden. Innerhalb einer expliziten Transaktion
können Sie dann über weitere Klassenmethoden von CATransaction Transaktionsparameter
setzen, mit denen sich die Animationserzeugung beeinflussen lässt.
In Listing 6.73 finden Sie ein Beispiel für eine explizite Transaktion in Core Animation, die die
Animationsdauer und den Animationsverlauf ändert. Innerhalb einer Transaktion müssen
Sie immer erst die Transaktionsparameter ändern und danach die Änderungen an den Layern durchführen, damit die Transaktionsparameter auch bei der Animationserzeugung
berücksichtigt werden. Der Animationsverlauf ist dabei ein Objekt der Klasse CAMediaTiming,
das eine Bézierkurve enthält, die die Punkte (0, 0) und (1, 1) miteinander verbindet. Diese
Kurve beschreibt die relative Geschwindigkeit der Animation in Beziehung zur relativen
Animationszeit.
[CATransaction begin];
[CATransaction setAnimationDuration:0.75];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:
kCAMediaTimingFunctionLinear];
theLayer.position = CGPointMake(100.0, 200.0);
[CATransaction commit];
Listing 6.73 Explizite Transaktion in Core Animation
679
6
Models, Layer, Animationen
6.3
Abbildung 6.30 stellt die Standardanimationsverläufe von Core Animation dar: 1 kCAMediaTimingFunctionLinear, 2 kCAMediaTimingFunctionEaseIn, 3 kCAMediaTimingFunctionEaseOut,
4 kCAMediaTimingFunctionEaseInEaseOut und 5 kCAMediaTimingFunctionDefault. Sie können
also beispielsweise durch den Verlauf kCAMediaTimingFunctionEaseInEaseOut erreichen, dass
die Animation langsam an- und ausläuft.
1
2
3
4
5
Core Animation
Dieses Beispiel ist zugegebenermaßen nicht unbedingt sinnvoll, da Sie ja hier auch die zwei
Transaktionen hintereinander schreiben können. Verschachtelte Transaktionen ergeben
mehr Sinn, wenn die inneren Transaktionen in Methoden erfolgen. Angenommen, die
Änderung der Transparenz soll immer eine halbe Sekunde dauern, dann könnten Sie den
entsprechenden Setter des Layers so überschreiben:
- (void)setOpacity:(CGFloat)inOpacity {
[CATransaction begin];
[CATransaction setAnimationDuration:0.5];
super.opacity = inOpacity;
[CATransaction commit];
}
Abbildung 6.30 Die verschiedenen Standardanimationsverläufe
Listing 6.76 Standardanimationszeit für Layer-Property festlegen
Bei der Animationserzeugung müssen Sie die Transaktionsparameter allerdings explizit
übernehmen und beispielsweise Listing 6.66 folgendermaßen anpassen:
Wenn Sie nun die Property opacity ändern, animiert das Programm diese Änderung immer
mit einer Dauer von einer halben Sekunde. Dabei können Sie diesen Setter von nahezu beliebigen Stellen Ihres Programmcodes aufrufen, und Sie müssen sich dabei insbesondere nicht
darum kümmern, ob sich die Anweisung bereits innerhalb einer Transaktion befindet.
CABasicAnimation *theAnimation = [CABasicAnimation animationForKeyPath:inKey];
theAnimation.duration = [CATransaction animationDuration];
theAnimation.timingFunction = [CATransaction animationTimingFunction];
theAnimation.fromValue = [inLayer valueForKey:kPartKey];
Listing 6.74 Übernahme der Transaktionsparameter
Transaktionen erlauben Ihnen über die Klassenmethoden valueForKey: und setValue:forKey: auch die Übergabe beliebiger Parameter, und Sie können über setCompletionBlock:
auch einen Block festlegen, den Core Animation nach der Beendigung aller Animationen
ausführt, die Ihr Programm innerhalb der Transaktion erzeugt hat.
Eine weitere praktische Eigenschaft von Transaktionen ist die Möglichkeit, sie zu schachteln.
Dadurch können Sie die Transaktionsparameter für bestimmte Layer-Änderungen anpassen. In Listing 6.75 sehen Sie ein einfaches Beispiel für verschachtelte Transaktionen, bei dem
die Positionsänderung in einer Dreiviertelsekunde und die Änderung der Transparenz in
einer halben Sekunde erfolgen.
[CATransaction begin];
[CATransaction setAnimationDuration:0.75];
theLayer.position = CGPointMake(100.0, 200.0);
[CATransaction begin];
[CATransaction setAnimationDuration:0.5];
theLayer.opacity = 0.5;
[CATransaction commit];
[CATransaction commit];
Über explizite Transaktionen können Sie auch die impliziten Animationen von Core Animation ausschalten. Wenn Sie Layer verändern, die nicht die Standard-Layer eines Views sind,
animiert Core Animation diese Änderung mit einer Dauer von einer Viertelsekunde. Häufig
ist dieses Verhalten jedoch unerwünscht. Über die Klassenmethode setDisableActions: lassen sich diese impliziten Animationen ausschalten, indem Core Animation die Erzeugung
von Aktionen unterdrückt. Listing 6.77 zeigt ein Beispiel für diese Methode.
[CATransaction begin];
[CATransaction setDisableActions:YES];
theLayer.position = CGPointMake(100.0, 200.0);
[CATransaction commit];
Listing 6.77 Aktionen und implizite Animationen unterdrücken
6.3.11 Die 3. Dimension
Im Gegensatz zu einem View befinden sich Layer in einem dreidimensionalen Raum. Über
die Property zPosition können Sie die Anordnung von überlappenden Layern zueinander
festlegen. Core Animation stellt Layer mit einer höheren z-Position vor Layern mit niedrigeren z-Positionen dar. Wenn sich Layer also überlappen, verdecken die Layer mit dem höheren Wert die mit den niedrigeren.
Im Gegensatz dazu legt die Property anchorPointZ die Lage des Layers in einem dreidimensionalen Raum fest (siehe Abbildung 6.31).
Listing 6.75 Verschachtelte Transaktionen
680
681
6
Models, Layer, Animationen
6.3
Core Animation
z-Achse besitzt. Etwas komplizierter ist es allerdings bei CATransform3DMakeRotation; hier
müssen Sie neben dem Winkel auch einen Richtungsvektor in Form einer dreidimensionalen Koordinate angeben. Dabei können Sie sich den Richtungsvektor als eine Linie vom Nullpunkt zu der Koordinate vorstellen. Die Ergebnismatrix beschreibt nun eine Drehung um
diesen Vektor. Beispielsweise beschreibt die Transformation
y
Layer
x
CATransform3DMakeRotation(M_PI / 4.0, 0.0, 0.0, 1.0)
eine 45°-Drehung um die z-Achse als Rotationsachse.
z
Abbildung 6.31 Dreidimensionales Koordinatensystem der Layer
Projektinformation
Im Github-Repository zum Buch finden Sie das Beispielprojekt Animation3D im Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Ausgabe_iOS8/Apps/iOS7/Animation3D.
Diese App erlaubt die Änderung der Propertys zPosition und anchorPointZ im Bereich von
–100 bis +100 über jeweils einen Slider. Dieses Projekt dient dabei weniger als Programmierbeispiel, auch wenn Sie darin viele Codeteile dieses Abschnitts wiederfinden. Es soll vielmehr
den Unterschied zwischen den beiden Propertys verdeutlichen. Starten Sie also das Projekt im
Simulator, und probieren Sie verschiedene Einstellungen aus; wenn Sie die Perspektive und die
Rotation aktivieren, sehen Sie damit auch sehr schön die Auswirkungen auf die dreidimensionale Darstellung.
Der Unterschied zwischen zPosition und anchorPointZ lässt sich bei Drehungen im dreidimensionalen Raum sehr gut visualisieren: Wenn anchorPointZ nicht null ist, dreht sich der
grüne Layer um den roten, wie die Erde um die Sonne. Ist diese Property hingegen null, drehen
sich die beiden Layer einfach nur um ihre x-Achse, wobei der Wert von zPosition keine Auswirkung auf die Drehung hat.
Layer mit Tiefgang
Wenn Sie einen Layer um die x- oder y-Achse drehen, sieht das Ergebnis indes nach wie vor
flach aus, und die Darstellung entspricht eher einer Stauchung. Das liegt daran, dass die Layer
zwar in einem dreidimensionalen Raum liegen, jedoch noch keine Perspektive haben. Bei einer
perspektivischen Darstellung sind die weiter hinten liegenden Bildteile kleiner als die weiter
vorn liegenden.
Diesen Effekt können Sie erreichen, indem Sie jeweils einen Anteil der z-Position zu jeweils der
x- und der y-Position addieren. Das verkleinert den hinten liegenden Teil des Layers und vergrößert den weiter vorn liegenden. Bei Layern mit Tiefe erzeugt das eine perspektivische Darstellung. Auch diese Berechnung lässt sich durch eine Transformationsmatrix durchführen.
Über die Property sublayerTransform legen Sie eine Transformation für die Sublayer eines
Layers fest. Da Core Animation diese Transformation zusätzlich zu der Transformation des
Sublayers anwendet, können Sie damit die Perspektivenberechnung durchführen.
CATransform3D theTransform = CATransform3DIdentity;
theTransform.m34 = -0.005;
theParentLayer.sublayerTransform = theTransform;
Mit dieser Transformation stellt theParentLayer seine Sublayer perspektivisch dar; er vergrößert oder verkleinert die Layer, deren anchorPointZ nicht null ist, und verzerrt um die x- oder yAchse gedrehte Layer.
Das Beispielprojekt Pie verwendet diesen Effekt. Wenn Sie den Pieview berühren, dreht er sich
um die x-Achse, wobei durch die beschriebene Transformation ein räumlicher Eindruck entsteht (siehe Abbildung 6.32).
Sie können zwar die Layer darin räumlich transformieren (drehen, strecken oder verschieben). Die Layer selbst bleiben hingegen flach. Beispielsweise verschwindet ein um 90° um
die y-Achse gedrehter Layer, weil seine Ausdehnung in x-Richtung in diesem Fall null ist.
Die affine Transformation eines Layers beschreibt die C-Struktur CATransform3D, die eine 4×4Matrix darstellt und auf die Sie über die Property transform zugreifen können. Sie können sie
analog zu den zweidimensionalen Transformationen von Core Graphics verwenden, da es
hierfür analoge Funktionen gibt. Beispielsweise entspricht der Funktion CGAffineTransformMakeScale die Funktion CATransform3DMakeScale, die jedoch einen weiteren Parameter für die
682
Abbildung 6.32 Layer mit Perspektive
683
6
Models, Layer, Animationen
6.4
Scrollviews und gekachelte Layer
Die Layer-Property transform lässt sich auch über Property-Animationen verändern. Dazu
erzeugen Sie die gewünschte Transformationsmatrix, die Sie danach über den ConvenienceKonstruktor valueWithCATransform3D: in ein NSValue-Objekt umwandeln müssen, weil die
Animationen ja Objekte und keine C-Strukturen erwarten.
theAnimation.toValue = @(M_PI / 2.0);
[theLayer addAnimation:theAnimation forKey:@"transform.rotation.x"];
CATransform3D theTransformation = CATransform3DMakeRotation( M_PI / 3.0, 1.0, 1.0,
0.0);
CABasicAnimation *theAnimation = [CABasicAnimation animation];
theTransform = CATransform3DScale(theTransform, 0.5, 0.5, 0.5);
theAnimation.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
theAnimation.toValue = [NSValue valueWithCATransform3D:theTransform];
[theLayer addAnimation:theAnimation forKey:@"transform"];
Sie können den Namen der Achse auch weglassen, um die Werte für mehrere Achsen gleichzeitig zu setzen. Die Werte in den Property-Animationen müssen Sie dann als NSArray mit
jeweils drei NSValue-Objekten übergeben:
Listing 6.79 Dreidimensionale Animation über einen Animationspfad
CABasicAnimation *theAnimation = [CABasicAnimation animation];
theAnimation.fromValue = @[@0.0, @0.0, @0.0];
theAnimation.toValue = @[@0.5, @0.5, @0.5];
[theLayer addAnimation:theAnimation forKey:@"transform.scale"];
Listing 6.78 Dreidimensionale Layer-Animation
Listing 6.80 Animierte Skalierung entlang mehrerer Achsen
Die Anweisungen in Listing 6.78 erzeugen eine Animation mit einer 60°-Drehung (π / 3) und
einer Stauchung auf die halbe Größe. Bei der Erzeugung einer Drehung im dreidimensionalen Raum müssen Sie ja die Rotationsachse beschreiben. Das ist in diesem Beispiel der Richtungsvektor (1, 1, 0), also die Linie vom Nullpunkt zum Punkt mit den Koordinaten (1, 1, 0).
Drehungen über die Hauptachsen
6.4 Scrollviews und gekachelte Layer
Ein View kann in der Regel nur so viel anzeigen, wie in seine Fläche passt. Wenn Sie Objekte
darstellen möchten, die größer als die verfügbare Fläche sind, können Sie dafür einen Scrollview verwenden. Ein Scrollview zeigt von seinen enthaltenen Views nur einen Ausschnitt an,
Vorsicht, der Inhalt dieses Kastens kann Spuren von Mathematik enthalten, und das Lesen
kann zu abstraktem Denken führen.
der auf die Fläche des Scrollviews passt. Der Nutzer kann diesen Ausschnitt durch Wisch-
Sie können Rotationen um einen beliebigen Vektor auch durch ein bis drei Rotationen an den
Hauptachsen x, y und z beschreiben, indem Sie den Rotationsvektor auch durch Drehungen
darstellen. Die Linie des Vektors (1, 1, 0) hat beispielsweise den Winkel 45° zur x-Achse, und
somit können Sie diesen Vektor als 45°-Drehung um die z-Achse ausdrücken. Die Rotation aus
Listing 6.78 können Sie also auch durch zwei Transformationen wie folgt beschreiben:
bewegen Sie zwei Finger aufeinander zu, um den Inhalt zu verkleinern und somit den ange-
CATransform3D theTransformation = CATransform3DMakeRotation(M_PI / 3.0, 1.0,
0.0, 0.0);
theTransform = CATransform3DRotate(theTransform, M_PI / 4.0, 0.0, 0.0, 1.0);
bewegungen verändern. Scrollviews unterstützen außerdem das Zoomen des Inhalts. Dabei
zeigten Ausschnitt zu vergrößern, oder Sie bewegen die Finger voneinander weg, um den
Inhalt zu vergrößern und somit den angezeigten Ausschnitt zu verkleinern. Diese beiden
Gesten bezeichnet man übrigens als Pinchgesten.
6.4.1 Scrollen und Zoomen
Wenn Sie einen Scrollview verwenden möchten, müssen Sie einen View mit der Klasse
UIScrollView anlegen. Scrollviews verfügen über eine Zeichenfläche für die Darstellung
In vielen Fällen reichen jedoch Rotationen, Skalierungen und Translationen an den drei
Hauptachsen aus, die Sie über spezielle Animationspfade auch einfacher beschreiben können. Die Pfade haben alle einen ähnlichen Aufbau: Sie beginnen mit dem Wort transform
(oder sublayerTransform), darauf folgen die Operation und der Name der Achse. Als Operationen stehen rotation, scale und translate zur Verfügung. Eine einfache Rotation um die
x-Achse können Sie beispielsweise so beschreiben:
ken oberen Ecke des Scrollviews. Dabei misst sich dieser Abstand von der Ecke des Scroll-
CABasicAnimation *theAnimation = [CABasicAnimation animation];
theAnimation.fromValue = @0.0;
Die weiße Scrollview-Fläche in der Abbildung entspricht dem Ausschnitt, den der Nutzer
sehen kann. Über die Property contentInset können Sie außerdem einen Rand um den
684
des Inhalts, die Sie über zwei Propertys beschreiben. Über die Property contentSize legen
Sie die Größe dieser Fläche fest und über contentOffset die relative Verschiebung zur linviews zu der Ecke der Fläche, so dass die beiden Koordinatenwerte immer positiv sind.
Abbildung 6.33 stellt den Contentoffset und die Contentsize durch einen beziehungsweise
zwei Pfeile dar.
685
6
Models, Layer, Animationen
6.4
Inhalt legen, dessen Breite Sie über den Größeninspektor des Interface-Builders festlegen
können. Falls Sie die Werte per Programmcode erzeugen wollen, können Sie über die Funktion UIEdgeInsetsMake die notwendige C-Struktur anlegen.
Scrollviews und gekachelte Layer
CGFloat theX = fmaxf((theSize.width - theContentSize.width) / 2.0, 0.0);
CGFloat theY = fmaxf((theSize.height - theContentSize.height) / 2.0, 0.0);
inView.contentInset = UIEdgeInsetsMake(theY, theX, theY, theX);
}
Inhaltsfläche
Listing 6.81 Kleinen Inhalt im Scrollview zentrieren
contentOffset
In diese Inhaltsfläche können Sie beliebige Views über die Methode addSubview: des Scrollviews legen. Dabei interpretiert Cocoa Touch die Framekoordinaten relativ zu der Inhaltsfläche des Scrollviews.
Scrollview
Damit Sie Scrollviews auch am lebenden Objekt kennenlernen, gibt es dazu ein Beispiel
zum Ausschneiden und Nachprogrammieren. Sie brauchen für das Beispiel ein möglichst
großes Bild. Dafür können Sie das Bild flower.jpg aus dem Beispielprojekt ScrollView oder
auch ein eigenes verwenden. Erstellen Sie in Xcode ein neues Projekt aus der Vorlage
Single View Application, und schalten Sie im Storyboard das Autolayout über den Dateiinspektor aus.
contentSize
Projektinformation
contentInsets
Abbildung 6.33 Inhaltsfläche des Scrollviews
Es kommt auf die Größe an
Der Scrollview betrachtet nur die durch die contentSize beschriebene Fläche als scrollbar.
Nach der Erzeugung des Scrollviews hat diese Fläche die Größe (0, 0). Wenn Sie diesen Wert
nicht ändern, sehen Sie zwar den Inhalt, können ihn allerdings nicht verschieben – beziehungsweise springt er nach einer Verschiebung wieder in seine Ursprungsposition zurück.
Denken Sie also immer daran, die Contentsize des Scrollviews zu setzen.
Diese Innenabstände sind allerdings nicht nur mit festen Werten nützlich; sie erlauben
Ihnen auch, den Contentview im Scrollview zu zentrieren, wenn seine Fläche kleiner als die
Fläche des Scrollviews ist. Die Methode updateInsetsForScrollView: aus Listing 6.81 zeigt den
dafür notwendigen Code. Da sie über die Funktion fmaxf verhindert, dass die Methode negative Werte als Insets verwendet, können Sie diese Methode auch dann getrost aufrufen,
wenn der Inhalt nur in einer oder sogar keiner Dimension zu klein ist.
- (void)updateInsetsForScrollView:(UIScrollView *)inView {
CGSize tSize = inView.bounds.size;
CGSize theContentSize = inView.contentSize;
686
Den Quellcode des folgenden Beispiels finden Sie im Github-Repository zum Buch im Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Ausgabe_iOS8/Apps/iOS5/
ScrollView.
Das Storyboard des Projekts enthält drei Viewcontroller für die unterschiedlichen Beispiele in
diesem Abschnitt. Sie sind jeweils nach dem zugehörenden Abschnitt benannt. Um den jeweiligen Viewcontroller in Aktion zu sehen, brauchen Sie nur in dessen Attributinspektor das
Häkchen Is Initial View Controller zu aktivieren. Zu Beginn sollten Sie also den ersten Viewcontroller mit dem Label Scrollen und zoomen aktivieren.
Nach dem Anlegen des Projekts fügen Sie das Bild zu der Gruppe Supporting Files hinzu.
Danach öffnen Sie das Storyboard und legen einen Scrollview in den View des Viewcontrollers. Der Interface-Builder zieht dieses neue Objekt genau in der Größe des Views an und
stellt auch die Autosizingmask so ein, dass der Scrollview immer die komplette Fläche des
Views belegt (siehe Abbildung 6.34).
Danach fügen Sie einen Imageview in den Scrollview ein. Über den Attributinspektor des
Imageviews können Sie unter Image das Bild einstellen, das der Imageview anzeigen soll.
Wenn Sie auf den Pfeil neben dem Eingabefeld klicken, können Sie das Bild auswählen, das
Sie zu dem Projekt hinzugefügt haben. Wenn Sie es auswählen, zeigt der View im InterfaceBuilder es wahrscheinlich verzerrt an. Öffnen Sie den Größeninspektor des Imageviews, und
ändern Sie die Höhe und die Breite des Views auf die entsprechenden Werte des Bildes. Das
Beispielbild flower.jpg hat eine Breite von 1.280 und eine Höhe von 850 Pixeln. Wenn Sie ein
687
6
Models, Layer, Animationen
eigenes Bild verwenden, sehen Sie seine Größe im Dateiinspektor dieses Bildes in der Rubrik
Image Properties.
6.4
Scrollviews und gekachelte Layer
- (void)viewWillAppear:(BOOL)inAppear {
[super viewWillAppear:inAppear];
self.scrollView.contentSize = self.contentView.frame.size;
}
Listing 6.82 Größe der Inhaltsfläche festlegen
Nach dieser Änderung können Sie den Bildausschnitt im Scrollview ändern, indem Sie mit
einem Finger über den Bildschirm des iPhones streichen. Zwar lässt sich nun jeder Ausschnitt des Bildes betrachten, hingegen nicht das gesamte Bild auf einmal, was sich allerdings über die Zoomfunktion des Scrollviews erreichen lässt. Um sie einzuschalten, müssen
Sie einen Skalierungsbereich, das Delegate und den zoombaren View festlegen.
Der Skalierungsbereich legt fest, wie stark der Nutzer den Inhalt verkleinern beziehungsweise vergrößern darf, und lässt sich im Attributinspektor unter Zoom einstellen. Dabei
sollte der Wert Min größer als 0 und kleiner oder gleich Max sein. Verwenden Sie hier 0,25
und 4 als Werte. Dann können Sie den Inhalt auf ein Viertel seiner Originalgröße verkleinern
beziehungsweise auf das Vierfache vergrößern. Per Programmcode können Sie über die Propertys minimumZoomScale und maximumZoomScale auf diese Werte zugreifen.
Sie müssen dem Scrollview jetzt nur noch mitteilen, welchen View er skalieren soll, was über
sein Delegate geschieht. Dafür verwenden Sie, wie in den meisten Fällen von Viewdelegates,
den Viewcontroller, der den Scrollview anzeigt. Erweitern Sie dazu die Deklaration des Viewcontrollers um das Protokoll UIScrollViewDelegate:
@interface ViewController : UIViewController<UIScrollViewDelegate>
Abbildung 6.34 Aufbau des Views für das Scrollview-Beispiel
Nachdem Sie diese Schritte ausgeführt haben, sollte der View im Interface-Builder so wie in
Abbildung 6.34 aussehen. Sie können die App auch starten. Sie zeigt Ihnen den gleichen Bildausschnitt an, jedoch macht sich der Scrollview noch nicht bemerkbar. Sie können so viel
über den Bildschirm wischen, wie Sie wollen. Das Bild lässt sich nicht bewegen.
Das liegt daran, dass Sie die Inhaltsfläche des Scrollviews noch nicht festgelegt haben und die
Contentsize somit noch 0 × 0 Punkte groß ist. Um diese Größe festzulegen, brauchen Sie ein
Outlet auf den Scrollview. Ziehen Sie also im Interface-Builder eine Verbindung vom Scrollview in den Header des Viewcontrollers, und nennen Sie dieses neue Outlet scrollView. Sie
können jetzt die Inhaltsfläche in der Methode viewWillAppear: festlegen. Anstatt dafür eine
feste Größe zu verwenden, sollten Sie lieber die Größe des Imageviews nehmen. Das geht am
einfachsten, indem Sie auch für den Imageview ein Outlet anlegen. Wenn Sie dieses Outlet
contentView nennen, können Sie die Größe der Inhaltsfläche festlegen, indem Sie der Property contentSize des Scrollviews die Größe des Imageviews zuweisen:
688
@property (weak, nonatomic) IBOutlet UIScrollView *scrollView;
@property (weak, nonatomic) IBOutlet UIImageView *contentView;
@end
Listing 6.83 Erweiterung des Viewcontrollers zu einem Scrollview-Delegate
Danach können Sie das Delegate auf die gewohnte Art über den Verbindungsinspektor des
Scrollviews festlegen. Über die Delegate-Methode viewForZoomingInScrollView: legen Sie
fest, welchen View der Scrollview vergrößern und verkleinern darf. Geben Sie hier den
Imageview so wie in Listing 6.84 zurück. Danach sollten Sie über Pinchgesten das Bild in der
App skalieren können.
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)inScrollView {
return self.contentView;
}
Listing 6.84 Zooming des Scrollviews einschalten
689
6
Models, Layer, Animationen
6.4
Scrollviews und gekachelte Layer
The One and Only
Damit ein Scrollview zoomen kann, müssen Sie folgende Punkte beachten:
왘 Legen Sie den minimalen und maximalen Skalierungsfaktor für den Scrollview fest.
왘 Weisen Sie dem Scrollview ein Delegate zu, in dessen Klasse Sie die Methode viewFor-
ZoomingInScrollView: implementieren.
Falls das Zoomen in einem Scrollview nicht funktionieren sollte, ist es ratsam, zunächst diese
beiden Punkte zu überprüfen.
Wie Sie gesehen haben, kann ein Scrollview allerdings nur einen seiner Subviews skalieren. Es
empfiehlt sich deshalb immer, nur einen direkten Subview in den Scrollview zu legen, in den
Sie alle anderen Subviews hineinlegen. Für diesen Containerview können Sie beispielsweise
ein Objekt der Klasse UIView verwenden. Über diesen View können Sie auch einfach die richtige Contentsize bestimmen, wie Sie in Listing 6.82 gesehen haben.
6.4.2 Scrollviews und Autolayout
Falls Sie Scrollviews zusammen mit Autolayout verwenden möchten, gibt es zwei mögliche
Ansätze: Sie können entweder die Größe über Restriktionen automatisch zuweisen oder der
Property contentSize eine explizite Größe zuweisen.
Projektinformation
Den Quellcode des folgenden Beispiels finden Sie im Github-Repository zum Buch im Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Ausgabe_iOS8/Apps/iOS7/AutolayoutScrollView.
Bei der ersten Variante legen Sie die Restriktionen zwischen den Rändern des Content- und
des Scrollviews mit dem Abstand 0 an. Das können Sie wie im Beispielprojekt AutolayoutScrollView über den Interface-Builder (siehe Abbildung 6.35) oder natürlich auch per Programmcode machen. Wenn Sie die Restriktionen über die Visual Format Language festlegen,
die Sie bereits in Abschnitt 4.4.5, »Autolayout-Restriktionen per Programmcode erstellen«
kennengelernt haben, sehen die Formate folgendermaßen aus:
[theScrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:
@"H:|-0-[contentView]-0-|" options:0 metrics:nil views:@{ @"contentView":
theContentView }];
[theScrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:
@"V:|-0-[contentView]-0-|" options:0 metrics:nil views:@{ @"contentView":
theContentView }];
Listing 6.85 Contentsize über Restriktionen festlegen
Abbildung 6.35 Die Contentsize über Restriktionen im Interface-Builder festlegen
Diese Restriktionen legen scheinbar die Größe des Contentviews auf die Ausmaße des Scrollviews fest. Tatsächlich bestimmt jedoch der Contentview beim Layoutprozess zunächst
seine Größe und der Scrollview verwendet danach diese Restriktionen, um die Contentsize
zu bestimmen. Die Layoutberechnung findet also an dieser Stelle von innen (dem Contentview) nach außen (dem Scrollview) statt. Bislang haben Sie Autolayout immer in der umgekehrten Richtung verwendet, wo die äußeren Views die Position und Größe der inneren
Views festgelegt haben.
Wenn Sie die Contentsize explizit setzen wollen, dürfen Sie keine Restriktionen zwischen
dem Scroll- und dem Contentview anlegen. Stattdessen müssen Sie abwarten, bis das Autolayout die Größen und Positionen der Views berechnet hat. Die Methode viewWillAppear: im
Beispielprojekt ist dafür allerdings zu früh, da anscheinend bei eingeschaltetem Autolayout
Cocoa Touch die Contentsize danach noch einmal auf (0, 0) setzt.
Sie können sich jedoch auch vom Viewcontroller informieren lassen, wenn sein View das
Layout abgeschlossen hat. Dazu überschreiben Sie die Methode viewDidLayoutSubviews; Listing 6.86 zeigt die entsprechende Implementierung.
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
CGSize theSize = self.contentView.frame.size;
self.scrollView.contentSize = theSize;
}
Listing 6.86 Aktualisieren der Contentsize bei Autolayout
690
691
6
Models, Layer, Animationen
6.4.3 Die Eventverarbeitung
Sie können jetzt in Ihren Scrollviews scrollen und zoomen. Viele Apps legen jedoch noch weitere Gesten auf den Scrollview, um seine Nutzung zu vereinfachen. Beispielsweise hat sich
die Double-Tab-Geste zum Vergrößern inzwischen eingebürgert. Dabei interpretiert die App
ein doppeltes Antippen des Scrollviews als Anweisung, den Inhalt zu vergrößern. Diese Geste
können Sie relativ leicht über die Klasse UITapGestureRecognizer umsetzen.
Für die Implementierung ziehen Sie einen solchen Recognizer aus der Bibliothek auf den
View im Scrollview. Der Interface-Builder legt daraufhin ein entsprechendes Objekt in der
obersten Ebene der Szene an. Öffnen Sie den Attributinspektor dieses Objekts, und stellen Sie
den Wert für Taps auf 2. Durch diese Einstellung legen Sie fest, dass der Recognizer erst bei
zwei aufeinanderfolgenden Taps seine Action-Methode aufruft, die Sie als Nächstes anlegen.
Ziehen Sie dazu eine Verbindung vom Recognizer-Objekt in den Deklarationsblock in der
Header-Datei des Viewcontrollers. In dem Dialog für die Verbindung wählen Sie unter
Connection den Punkt Action aus, und als Namen der Action-Methode geben Sie zoomIn
ein. Die Implementierung dieser Methode verdoppelt einfach den Skalierungswert des
Scrollviews:
6.4
Wenn Sie einen Slider in den Scrollview des Beispielprojekts legen, können Sie ihn verstellen
und trotzdem auch das Bild scrollen. Obwohl die Geste zum Verstellen des Sliders der zum
Scrollen entspricht, kann Cocoa Touch Ihre Fingerbewegung immer dem richtigen Eingabeelement zuordnen. Dabei geschieht die Zuordnung einfach über das Element unter dem Finger. Wenn die Fingerbewegung auf dem Slider beginnt, bekommt dieser alle Ereignisse.
Ansonsten sendet Cocoa Touch die Ereignisse an den Scrollview.
Ob der Scrollview oder der View (also hier der Slider) die Ereignisse verarbeiten darf,
entscheidet Cocoa Touch anhand von zwei Methoden des Scrollviews. Sie können diese
beiden Methoden in Unterklassen überschreiben, um die Ereignisverarbeitung des Scrollviews an Ihre Bedürfnisse anzupassen. Dabei entscheidet der Scrollview anhand der
Methode touchesShouldBegin:withEvent:inContentView:, ob der angegebene View überhaupt Ereignisse empfangen soll. Die Implementierung dieser Methode in der Klasse
UIScrollView liefert immer YES zurück.
userInteractionEnabled
in Contentview
- (IBAction)zoomIn:(id)inRecognizer {
CGFloat theScale = self.scrollView.zoomScale;
[self.scrollView setZoomScale:theScale * 2.0 animated:YES];
}
NO
YES
touchesShouldBegin:
withEvent:
inContentView:
Listing 6.87 Action-Methode für Double-Tap
So weit, so einfach. Analog dazu können Sie noch weitere Gesture-Recognizer in den Scrollview legen, um weitere Gesten auszuwerten. Das geht so lange gut, wie die Gesten der Recognizer nicht mit denen des Scrollviews kollidieren. Wenn Sie beispielsweise einen Recognizer
der Klasse UIPanGestureRecognizer in den Scrollview legen, können Sie nicht mehr scrollen.
Das liegt daran, dass der Scrollview und der Recognizer auf die gleichen Gesten reagieren. Der
Scrollview wertet zwar alle Fingerbewegungen auf seiner Fläche aus, er verschiebt seinen
Inhalt allerdings nur, wenn er die Ereignisse nicht an seine Subviews weiterleitet. Er kann also
immer nur eins gleichzeitig machen: Entweder scrollt er, oder er leitet die Ereignisse weiter.
Scrollviews und gekachelte Layer
NO
YES
canCancelContentTouches
YES
NO
NO
touchesShouldCancel
InContentView:
Scrollviews und Gesten
Solche Kollisionen zwischen Scrollview- und anderen Gesten treten bei der App-Entwicklung
unter Cocoa Touch relativ häufig auf. Der erste und wichtigste Schritt zu ihrer Auflösung ist
eine gute Planung. Überlegen Sie sich also zuerst klare Kriterien, wie sich die Scrollview-Gesten
von den anderen Gesten unterscheiden lassen, bevor Sie zur Implementierung schreiten. Diese
Kriterien erleichtern Ihnen nicht nur die Implementierung, sondern schlagen sich meistens
auch in einer besseren Bedienbarkeit der App nieder.
692
YES
Ereignisverarbeitung
durch Contentview
touchesCancelled:
withEvent:
an Contentview senden
Ereignisverarbeitung
durch Scrollview
Abbildung 6.36 Ereigniszuordnung des Scrollviews
693
6
Models, Layer, Animationen
Der Scrollview ruft diese Methode allerdings nur für Subviews auf, deren Property userInteractionEnabled den Wert YES hat. Über die Property canCancelContentTouches können Sie festlegen, ob sich der Scrollview die Ereignisverarbeitung zurückholen darf, wenn der Nutzer den
Finger weit genug über den Subview hinausschiebt. Diese Geste kann er ja in diesem Fall als
Scrollgeste verstehen. Diese Entscheidung trifft die Methode touchesShouldCancelInContentView:, die der Scrollview befragt, ob er nicht doch lieber scrollen darf. Wenn sie YES zurückliefert, bricht der Scrollview die Ereignisverarbeitung im Subview ab, indem er an diesen View
ein touchesCancelled:withEvent: sendet. Die Standardimplementierung dieser Methode liefert nur dann NO, wenn der Contentview die Klasse UIControl oder eine Unterklasse davon
hat. Der Slider und der Scrollview im Beispiel harmonieren so schön miteinander, weil die
Klasse UISlider eine Unterklasse von UIControl ist. Den kompletten Entscheidungsablauf
sehen Sie in Abbildung 6.36.
Das folgende Beispiel verdeutlicht, wie Sie das Verhalten eines Scrollviews so beeinflussen
können, dass er mit Ihren Views zusammenarbeitet. Dazu erweitern Sie das Beispielprojekt,
indem Sie die Viewcontroller-Szene im Storyboard duplizieren, also kopieren und wieder
einfügen. Legen Sie die Kopie als initialen Viewcontroller des Storyboards fest, und löschen
Sie den enthaltenen Imageview und den Slider. Stattdessen legen Sie einen neuen View
(Klasse UIView) in den Scrollview, dessen Fläche größer als die des Scrollviews ist. In diesen
View legen Sie einen weiteren View, der eine Fläche von 200 × 200 Punkten haben sollte.
Achten Sie darauf, dass Sie bei beiden Views userInteractionEnabled eingeschaltet haben.
Außerdem sollten Sie dem kleineren View eine andere Hintergrundfarbe geben, um ihn vom
größeren unterscheiden zu können.
6.4
Scrollviews und gekachelte Layer
[[UIColor redColor] setStroke];
[thePath moveToPoint:self.startPoint];
[thePath addLineToPoint:self.endPoint];
[thePath stroke];
}
- (void)touchesBegan:(NSSet *)inTouches withEvent:(UIEvent *)inEvent {
UITouch *theTouch = [inTouches anyObject];
self.startPoint = [theTouch locationInView:self];
}
- (void)touchesMoved:(NSSet *)inTouches withEvent:(UIEvent *)inEvent {
UITouch *theTouch = [inTouches anyObject];
self.endPoint = [theTouch locationInView:self];
[self setNeedsDisplay];
}
- (void)touchesEnded:(NSSet *)inTouches withEvent:(UIEvent *)inEvent {
UITouch *theTouch = [inTouches anyObject];
self.endPoint = [theTouch locationInView:self];
[self setNeedsDisplay];
}
@end
Der kleinere View bekommt die Klasse LineView, deren Deklaration und Implementierung
Sie in Listing 6.88 beziehungsweise in Listing 6.89 finden.
Listing 6.89 Implementierung der Klasse »LineView«
@interface LineView : UIView
Geste. Sie verbinden also den Punkt der ersten Berührung des Views mit dem Punkt, an dem
Die Views dieser Klasse zeichnen jeweils eine rote Linie vom Start- zum Endpunkt einer
der Nutzer den Finger wieder vom Bildschirm genommen hat. Weisen Sie diese Klasse dem
@property(nonatomic) CGPoint startPoint;
@property(nonatomic) CGPoint endPoint;
@end
Listing 6.88 Deklaration der Klasse »LineView«
@implementation LineView
@synthesize startPoint;
@synthesize endPoint;
kleinen View über seinen Identitätsinspektor zu. Den größeren View sollten Sie außerdem
mit dem Outlet contentView des Viewcontrollers verbinden. Dazu müssen Sie allerdings erst
den Typ der Property von UIImageView auf UIView oder id ändern. Die Struktur des neuen
Views sehen Sie in Abbildung 6.37.
Im Attributinspektor des Scrollviews schalten Sie die Einstellungen Delays Content
Touches und Cancellable Content Touches aus. Wenn Sie die App mit dieser Einstellung starten, können Sie zwar rote Linien im Subview ziehen, jedoch nicht den Inhalt des
Scrollviews verschieben. Das liegt daran, dass der Scrollview die Ereignisse immer an seine
Subviews weiterleitet. Da er allerdings begonnene Gesten nicht abbrechen darf, hat er keine
Möglichkeit, die Gesten in dem großen View zu beenden und die Fingerbewegungen in
- (void)drawRect:(CGRect)inRect {
UIBezierPath *thePath = [UIBezierPath bezierPath];
694
Scrollbewegungen umzusetzen. Das können Sie leicht nachvollziehen, wenn Sie den Entscheidungsweg für diese Einstellung in Abbildung 6.36 verfolgen.
695
6
Models, Layer, Animationen
6.4
Scrollviews und gekachelte Layer
@implementation UIView(ScrollView)
- (BOOL)shouldCancelContentTouches {
return YES;
}
@end
@implementation ScrollView
- (BOOL)touchesShouldCancelInContentView:(UIView *)inView {
return [inView shouldCancelContentTouches] &&
[super touchesShouldCancelInContentView:inView];
}
@end
Listing 6.90 Abbruch der Gestenverarbeitung über die Views
Abbildung 6.37 Beispielview für die Ereignisverarbeitung
Schalten Sie nun Cancellable Content Touches im Attributinspektor des Scrollviews ein.
Jetzt können Sie wieder scrollen, allerdings allenfalls einen kurzen roten Strich ziehen. Da
der Lineview kein Control ist, liefert die Methode touchesShouldCancelInContentView: für diesen View immer YES und bricht die Gestenverarbeitung ab. An dieser Stelle können Sie durch
eine eigene Implementierung der Methode in einer Unterklasse von UIScrollView eingreifen. Diese Implementierung sollte für den Lineview NO zurückliefern, damit dieser die Geste
weiterverarbeiten kann.
Erstellen Sie dazu eine Unterklasse von UIScrollView. Es gibt mehrere Möglichkeiten, wie
Sie die Methode touchesShouldCancelInContentView: überschreiben können. Der hier vorgestellte Weg vermeidet Abhängigkeiten zwischen der Scrollview-Unterklasse und der Klasse
LineView. Dafür definieren Sie einfach eine neue Methode namens shouldCancelContentTouches in einer Kategorie der Klasse UIView. Dadurch können Sie diese Nachricht an alle
Views schicken. Die Kategorie können Sie in die Implementierungsdatei der neuen Scrollview-Klasse schreiben, so dass sie folgendermaßen aussieht:
696
Wenn Sie diese Unterklasse für Ihre Scrollviews verwenden, können deren Subviews selbst
festlegen, ob der Scrollview eine Geste abbrechen darf. Bei Lineviews soll das beispielsweise
nicht möglich sein. Dafür braucht diese Klasse nur die Methode shouldCancelContentTouches
zu überschreiben und NO als Ergebnis zu liefern. Wenn Sie also die Methode in der Klasse
LineView entsprechend implementieren und im Identitätsinspektor des Scrollviews im
Storyboard die Klasse ScrollView einstellen, dann können Sie im LineView Linien ziehen und
mit der restlichen Fläche des Scrollviews scrollen.
Verzögerte Gestenverarbeitung
Es gibt noch eine weitere Möglichkeit, wie der Scrollview das Ziel einer Nutzereingabe erkennen kann. Dabei wartet er eine kurze Zeitspanne, um die Intention des Nutzers zu erraten. Sie
können diese Option über die Einstellung Delays Content Touches im Attributinspektor des
Interface-Builders oder die Property delaysContentTouches einschalten. Dann interpretiert
der Scrollview eine Bewegung des Fingers über den Scrollview als Scrollgeste, wenn der Nutzer
den Finger von Beginn an bewegt hat. Wenn er hingegen den Scrollview berührt, ohne über
den Bildschirm zu streichen, und erst nach einem kurzen Moment den Finger bewegt, leitet
der Scrollview die Touchevents an den Contentview weiter.
6.4.4 Scharfe Kurven
In Abschnitt 6.4.1, »Scrollen und Zoomen«, haben Sie gesehen, wie Sie die Zoomfunktion des
Scrollviews nutzen können. Wenn Sie allerdings bei dem Lineview-Beispiel die größte Vergrößerung einstellen, stellen Sie fest, dass der View die Linie extrem unscharf darstellt. Das
697
6
Models, Layer, Animationen
6.4
liegt daran, dass der Scrollview die Vergrößerung des Inhalts über die Transformationsmatrix des Views beziehungsweise dessen Layer realisiert. Diese Transformation ändert jedoch
nicht die Anzahl der Pixel im Layer, sondern vergrößert nur jeden Pixel. Die Unschärfe entsteht schließlich durch das Antialiasing. Antialiasing bezeichnet Algorithmen in der Computergrafik zur Kantenglättung. Dabei verwenden die Verfahren zum Zeichnen der Punkte
Farbabstufungen, wodurch gewollte Unschärfen entstehen, die Treppeneffekte bei schrägen
oder gebogenen Linien vermeiden oder abschwächen.
Abbildung 6.38 Linie mit 1 und ohne 2 Antialiasing sowie pixelgenau 3
In Abbildung 6.38 sehen Sie, wie der Antialiasingeffekt bei einer vergrößerten Linie aussieht
1. Diese Unschärfen wirken manchmal sehr störend, weswegen Sie sie auch abschalten
können 2. Dazu verwenden Sie die Core-Graphics-Funktion CGContextSetAllowsAntialiasing.
Wenn Sie die Linie in der Klasse LineView beispielsweise ohne Antialiasing zeichnen möchten,
können Sie die drawRect:-Methode folgendermaßen ändern:
- (void)drawRect:(CGRect)inRect {
UIBezierPath *thePath = [UIBezierPath bezierPath];
Scrollviews und gekachelte Layer
sion genau der Größe, für den Wert 2 hat der Layer jeweils doppelt so viele Pixel usw. Der Standard-Layer eines Views hat für ein normales Display den Wert 1 und auf einem Retina-Display
den Wert 2.
Sie können den Wert dieser Property erhöhen, um eine genauere Darstellung zu erhalten.
Allerdings vervierfachen Sie mit jeder Stufe auch den Speicherverbrauch des Layers. Sie sollten
diese Möglichkeit also allenfalls für kleine Layer in Betracht ziehen.
Damit die Klasse LineView diese Layer-Klasse verwendet, müssen Sie natürlich ihre Klassenmethode layerClass überschreiben, wie Sie das bereits in Abschnitt 6.3.1, »Layer«, gemacht
haben. Diesmal verwenden Sie allerdings die Klasse CATiledLayer. Außerdem müssen Sie das
QuartzCore-Framework zum Projekt hinzufügen und dessen Header in LineView.m importieren (siehe Abschnitt 6.3, »Core Animation«). Listing 6.92 zeigt den Beginn der Implementierung dieser Klasse.
#import "LineView.h"
#import <QuartzCore/QuartzCore.h>
@implementation LineView
+ (id)layerClass {
return [CATiledLayer class];
}
Listing 6.92 Verwendung von »CATiledLayer« als Layer-Klasse
CGContextSetAllowsAntialiasing(UIGraphicsGetCurrentContext(), NO);
[[UIColor redColor] setStroke];
[thePath moveToPoint:self.startPoint];
[thePath addLineToPoint:self.endPoint];
[thePath stroke];
}
Listing 6.91 Ausschalten des Antialiasings
Die Darstellung der Linie ist allerdings weder mit an- noch mit abgeschaltetem Antialiasing
zufriedenstellend. Sie sieht entweder unscharf oder stufig aus. Eine gute Darstellung sollte
vielmehr Bild 3 in Abbildung 6.38 entsprechen. Dazu müssen Sie dem View beibringen, dass
er bei einer Vergrößerung mehr Pixel für die Darstellung verwendet. Mit der Klasse CATiledLayer stellt Ihnen Core Animation dafür eine Möglichkeit zur Verfügung.
Sie brauchen jetzt nur noch den Layer in der Methode awakeFromNib zu konfigurieren. Sie
müssen dem Layer nämlich mitteilen, wie stark Sie seinen Inhalt vergrößern wollen. Dazu
verwenden Sie die Property levelsOfDetailBias, die standardmäßig den Wert 0 hat. Dieser
Wert entspricht der Originalgröße des Inhalts, und bei größeren Werten stellt der Layer
jeweils eine höhere Detailstufe zur Verfügung. Dabei verdoppelt jede Stufe jeweils die Detailgenauigkeit ihres Vorgängers. Der Wert 1 entspricht also der doppelten und der Wert 2 der
vierfachen Genauigkeit. Da Sie die maximale Vergrößerung des Scrollviews auf 4 eingestellt
haben, sollte also der Wert 2 ausreichen. Bei einer maximalen Vergrößerung von 8 sollten Sie
hingegen den Wert 3 wählen.
- (void)awakeFromNib {
[super awakeFromNib];
CATiledLayer *theLayer = (CATiledLayer *)self.layer;
Mehr Pixel für’s Geld
Über die Property contentsScale können Sie die Anzahl der Pixel eines Layers im Verhältnis zu
seiner Größe ändern. Für den Wert 1 entspricht die Anzahl der Pixel des Layers in jeder Dimen-
698
theLayer.levelsOfDetailBias = 2;
}
Listing 6.93 Einstellung der Detailgenauigkeit des Layers
699
6
Models, Layer, Animationen
6.4
Sie können außerdem die Anzahl der Detailebenen des Layers über die Property levelsOfDetail festlegen. Sie hat den Standardwert 1, was bedeutet, dass der Layer eine Detailebene
besitzt. Diese Detailebene entspricht der höchsten Detailstufe, und der Layer stellt zum
Zeichnen immer eine Fläche mit so vielen Pixeln zur Verfügung, wie dieser Stufe entsprechen.
Beispielsweise hat der Lineview im Projekt eine Größe von 200 × 200 Pixeln, und der Layer
stellt zwei Vergrößerungsstufen – also eine vierfache Vergrößerung – noch pixelgenau dar.
Somit hat die Zeichenfläche 800 × 800 Pixel, und alle Zeichenoperationen erfolgen in dieser
Auflösung, auch wenn Sie die Fläche verkleinern. In diesem Fall verkleinert Cocoa Touch die
Pixelgrafik auf die Anzeigegröße. Wenn Sie einen höheren Wert für die Property levelsOfDetail angeben, verwendet der Layer für kleinere Skalierungsstufen auch kleinere Pixelflächen. Dabei halbiert er für jede Stufe jeweils die Breite und die Höhe ihres Vorgängers. Wenn
Sie beispielsweise levelsOfDetail auf 5 setzen, hat der Layer folgende Detailstufen:
Stufe
Skalierungsfaktor
Auflösung
0
4
800 × 800
1
2
400 × 400
2
1
200 × 200
3
1/2
100 × 100
4
1/4
50 × 50
Scrollviews und gekachelte Layer
muss dabei ja das Bild unkomprimiert vorliegen, und Sie können seinen Speicherbedarf folgendermaßen berechnen:
Breite × Höhe × Anzahl der Farben
Angenommen, Sie möchten ein RGB-Bild mit 2.000 × 3.000 Pixeln in Echtfarben und mit
einem Alphakanal anzeigen. Dann hat es vier Farben (Rot, Grün, Blau und Alpha), und es verbraucht 2.000 × 3.000 × 4 = 24 Millionen Byte. Diese Datenmenge führt auf vielen Geräten
bereits zu Speicherwarnungen, auch wenn sie noch wesentlich mehr Speicher zur Verfügung
haben. Ein entsprechendes Bild, das auf einem iPad 2 den kompletten Bildschirm belegt,
braucht hingegen nur 1.024 × 768 × 4 ≈ 3 Millionen Byte. Das unbedachte Laden kompletter
Bilder kann also sehr problematisch sein.
Auch bei diesem Problem hilft die Klasse CATiledLayer. Wie ihr Name schon verrät, unterteilt
sie den Inhalt in gleich große Rechtecke oder auch Kacheln (Tiles) und stellt nur den Inhalt
der Rechtecke dar, die die angezeigte Fläche des Layers schneiden. Ein Beispiel dafür finden
Sie in Abbildung 6.39. Das große weiße Rechteck über der Blüte stellt die angezeigte Fläche
des Layers dar, und die dünnen weißen Linien veranschaulichen die Unterteilungsrechtecke
des Layers. Dabei zeichnet der Layer nur die nicht ausgegrauten Rechtecke. Über die Property tileSize der Klasse CATiledLayer legen Sie die Größe der Kacheln fest.
Tabelle 6.8 Die Detailstufen des Beispielprojekts
Detailstufen
Im Beispielprojekt machen sich die höheren Detailstufen nicht bemerkbar, weil die Grafik
dafür zu wenig Details besitzt. Bei fein strukturierten Grafiken können Sie indes durch höhere
Detailstufen durchaus eine Verbesserung der Darstellung erreichen.
Für die glatten Detailstufen zeichnet Cocoa Touch jeweils den Inhalt des Layers. Bei den Zwischenwerten verkleinert der Layer jeweils die Grafik mit dem nächsthöheren Skalierungsfaktor. Eine Skalierung von 1,5 stellt der Layer beispielsweise über eine Verkleinerung der Grafik
mit dem Faktor 2 dar.
Abbildung 6.39 Unterteilung der Zeichenfläche in Rechtecke
6.4.5 Ganz großes Kino
Wenn Sie große Bilder über einen gekachelten Layer darstellen möchten, sollten Sie das Bild
bereits als Kacheln laden. Andernfalls müssten Sie ja das komplette Bild in den Speicher
laden, und der gekachelte Layer brächte keinen Vorteil.
Scrollviews stellen Inhalte dar, die größer als die verfügbare Fläche sind. Die Verwendung der
Klasse CATiledLayer ermöglicht dabei sogar die Darstellung von Inhalten, die so groß sind,
dass sie nicht komplett in den verfügbaren Speicher des Gerätes passen. Für die Anzeige
Das Beispielprojekt ScrollView enthält die Klasse TiledImageView, die eine gekachelte Version
des Blumenbildes anzeigt. Sie legt die Klasse CATiledLayer auf die bekannte Weise als LayerKlasse fest und initialisiert die Kachelgröße in der Methode awakeFromNib.
700
701
6
Models, Layer, Animationen
6.4
const static CGFloat kImageWidth = 320.0;
const static CGFloat kImageHeight = 170.0;
if(CGRectIntersectsRect(inRect, theTileFrame)) {
NSString *theFile = [NSString
stringWithFormat:@"flower_%ux%u",
i, j];
NSString *thePath = [theBundle pathForResource:theFile ofType:
@"jpg"];
UIImage *theImage = [UIImage imageWithContentsOfFile:thePath];
+ (Class)layerClass {
return [CATiledLayer class];
}
- (void)awakeFromNib {
[super awakeFromNib];
CATiledLayer *theLayer = (CATiledLayer *)self.layer;
theLayer.tileSize = CGSizeMake(kImageWidth, kImageHeight);
}
Scrollviews und gekachelte Layer
[theImage drawAtPoint:theTileFrame.origin];
}
}
}
}
Listing 6.95 Zeichnen der Kacheln
Listing 6.94 Initialisierung des Tiled-Layers für die gekachelte Darstellung
Den Inhalt für den Layer stellen Sie über die Methode drawRect: zur Verfügung. Im Gegensatz zu den bisherigen Implementierungen dieser Methode in diesem Buch ist hier allerdings der Parameter relevant. Er enthält das Rechteck der Kachel, die der View zeichnen soll.
Wenn die Kacheln kleiner als die sichtbare Fläche des Views sind, ruft Cocoa Touch diese
Methode mehrmals mit unterschiedlichen Rechtecken auf.
Mit dieser Implementierung stellt der View das Bild dar. Dass er es dabei tatsächlich über einzelne Kacheln zeichnet, sehen Sie einerseits an der Log-Ausgabe, die die Rechtecke der
Kacheln anzeigt. Andererseits sehen Sie aber auch bei der ersten Anzeige auf dem Bildschirm
oder beim Scrollen, wie der View die einzelnen Rechtecke mit den Bildern füllt. An der LogAusgabe sehen Sie außerdem, dass der Layer die Rechtecke nicht von oben links nach unten
rechts zeichnet.
In Listing 6.95 sehen Sie die Implementierung der drawRect:-Methode der Klasse TiledImageView. Sie berechnet nacheinander die Rechtecke für alle Kacheln des Bildes. Bei jeder Kachel,
die das Rechteck des darzustellenden Bereiches schneidet, lädt die Methode das entsprechende Bild und zeichnet es in den aktuellen Grafikkontext.
Achtung, Cache
Die Implementierung in Listing 6.95 liest die Kachelbilder nicht über den Convenience-Konstruktor imageNamed: von UIImage, da Cocoa Touch sich diese Bilder im Speicher merkt (sie also
cacht). Bei sehr großen Bildern würde das wieder zu Speicherwarnungen führen, die ja die
gekachelte Darstellung gerade vermeiden soll. Sie sollten also diese Methode bei großen oder
vielen Bildern nur mit Vorsicht einsetzen.
- (void)drawRect:(CGRect)inRect {
CGRect theTileFrame = CGRectMake(0.0, 0.0, kImageWidth, kImageHeight);
NSBundle *theBundle = [NSBundle mainBundle];
NSLog(@"drawRect:%@", NSStringFromCGRect(inRect));
for(NSUInteger i = 0; i < 5; ++i) {
for(NSUInteger j = 0; j < 4; ++j) {
theTileFrame.origin.x = j * kImageWidth;
theTileFrame.origin.y = i * kImageHeight;
Darstellung über den Layer
Es ist natürlich auch möglich, den Layer-Inhalt über die Delegate-Methode drawLayer:inContext: bereitzustellen. Allerdings müssen Sie das Clipping-Rechteck selbst bestimmen und das
Koordinatensystem spiegeln. Die Implementierung über die Delegate-Methode des Layers
ähnelt der Implementierung in Listing 6.95 sehr (die Änderungen sind hervorgehoben):
- (void)drawLayer:(CALayer *)inLayer inContext:(CGContextRef)inContext {
CGContextSaveGState(inContext);
CGContextScaleCTM(inContext, 1.0, –1.0);
CGContextTranslateCTM(inContext, 0.0, -CGRectGetHeight(self.bounds));
CGRect theRect = CGContextGetClipBoundingBox(inContext);
CGRect theTileFrame = CGRectMake(0.0, 0.0, kImageWidth, kImageHeight);
NSBundle *theBundle = [NSBundle mainBundle];
NSLog(@"drawLayer:inContext:%@", NSStringFromCGRect(theRect));
for(NSUInteger i = 0; i < 5; ++i) {
for(NSUInteger j = 0; j < 4; ++j) {
theTileFrame.origin.x = j * kImageWidth;
theTileFrame.origin.y = i * kImageHeight;
if(CGRectIntersectsRect(theRect, theTileFrame)) {
702
703
6
Models, Layer, Animationen
6.4
NSString *theFile = [NSString stringWithFormat:@"flower_%ux%u",
4 – i, j];
NSString *thePath = [theBundle pathForResource:theFile
ofType:@"jpg"];
UIImage *theImage = [UIImage imageWithContentsOfFile:thePath];
Scrollviews und gekachelte Layer
Dokument anzeigt. Dieser View zeichnet dabei alle Seiten des Dokuments untereinander,
wobei er sie durch graue Rahmen voneinander trennt.
Projektinformation
Den Quellcode des folgenden Beispiels finden Sie im Github-Repository zum Buch im Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Ausgabe_iOS8/Apps/iOS5/PDFView.
CGContextDrawImage(inContext, theTileFrame, theImage.CGImage);
}
}
}
CGContextRestoreGState(inContext);
}
Auch diese Methode befindet sich in der Klasse TiledImageView, und Sie können sie aktivieren,
indem Sie den Wert des Makros USE_DELEGATION am Anfang der Datei von 0 auf 1 setzen.
Sie können den Aufbau des Inhalts übrigens mit kleinen Kachelgrößen sehr schön beobachten. Ändern Sie dazu einfach in der Methode awakeFromNib die Zuweisung in:
theLayer.tileSize = CGSizeMake(10.0, 10.0);
Der Layer zeichnet dann sehr kleine Kacheln, und Sie können die Reihenfolge beim Aufbau
erkennen. Er zeichnet die Kacheln kreisförmig von innen nach außen (siehe Abbildung 6.40).
Außerdem sehen Sie an diesem Beispiel, dass bei der Klasse TiledImageView die Kachelgröße
des Layers unabhängig von der Größe der Einzelbilder ist.
Dabei gibt die Methode drawPage:inRect:context: in Listing 6.96 die angegebene PDF-Seite
in den angegebenen Grafikkontext aus und skaliert die Seite so, dass sie in das übergebene
Rechteck hineinpasst. Für die Darstellung verwendet sie drei Core-Graphics-Funktionen.
CGPDFPageGetDrawingTransform berechnet eine Transformationsmatrix für die Seite, so dass
die Seite in das angegebene Rechteck passt. Diese Matrix wendet die Methode auf den Grafikkontext an, bevor sie die Seite über die Funktion CGContextDrawPDFPage zeichnet.
- (void)drawPage:(CGPDFPageRef)inPage inRect:(CGRect)inRect context:
(CGContextRef)inContext {
CGPDFBox theBox = kCGPDFMediaBox;
CGAffineTransform theTransform = CGPDFPageGetDrawingTransform(inPage,
kCGPDFMediaBox, inRect, 0, YES);
CGContextConcatCTM(inContext, theTransform);
CGContextDrawPDFPage(inContext, inPage);
}
Listing 6.96 Zeichnen einer einzelnen PDF-Seite
Die drawRect:-Methode kann nun diese Methode verwenden, um alle Seiten auf die Fläche
des Views zu zeichnen. Dazu initialisiert sie zunächst einige Variablen. Neben dem Grafikkontext und dem Bounds-Rechteck braucht die Methode die Anzahl der Seiten, die sie in der
Variablen theCount ablegt. Außerdem berechnet sie in der Variablen thePageFrame das Rechteck für eine einzelne Seite. Es hat die gleiche Position und Breite wie das Bounds-Rechteck.
Da der View das komplette Dokument anzeigt, entspricht die Höhe einer Seite dem Verhältnis von der Gesamthöhe zu der Anzahl der Seiten.
Abbildung 6.40 Der Tiled-Layer bei der Arbeit
6.4.6 PDF-Dateien anzeigen
Sie können kachelnde Layer indes nicht nur für Bilder, sondern natürlich auch für beliebige
Inhalte verwenden. Beispielsweise kann auch die Anzeige von PDF-Dateien sehr viel Speicher verbrauchen. Core Graphics erlaubt Ihnen, mit relativ wenig Aufwand PDF-Dokumente
darzustellen. Im Beispielprojekt PDFView finden Sie die gleichnamige Klasse, die ein PDF-
704
CGContextRef theContext = UIGraphicsGetCurrentContext();
CGRect theBounds = self.bounds;
size_t theCount = self.countOfPages;
CGFloat theHeight = CGRectGetHeight(theBounds) / theCount;
CGRect thePageFrame = theBounds;
thePageFrame.size.height = theHeight;
Listing 6.97 Variablendefinitionen für das Zeichnen der PDF-Seiten
705
6
Models, Layer, Animationen
6.4
Scrollviews und gekachelte Layer
PDF-Dokumente und Seiten
Die Methode countOfPages liefert die Anzahl der Seiten im PDF-Dokument des Views, und der
View speichert das Dokument in dem Attribut document ab, zu dem es die entsprechenden
Accessoren document und setDocument: gibt. Über die Funktion CGPDFDocumentGetNumberOfPages ermitteln Sie die Seitenzahl des Dokuments:
- (size_t)countOfPages {
return CGPDFDocumentGetNumberOfPages(self.document);
}
Die Funktion CGPDFDocumentGetPage liefert Ihnen einzelne Seiten aus dem Dokument, wobei
Sie im zweiten Parameter die Seitenzahl der gewünschten Seite übergeben. Achtung: Dabei
beginnt die Zählung nicht wie üblich bei 0, sondern bei 1.
Die Methode braucht nur die Teile des Dokuments zu zeichnen, die in einer der angezeigten
Kacheln liegen. Die Rechtecke dieser Kacheln bekommen Sie dabei als Parameterwerte der
Methode drawRect: übergeben. Zum Zeichnen müssen Sie also die Seiten ermitteln, die sich
mit dem übergebenen Rechteck schneiden. Dazu berechnen Sie für jede PDF-Seite ihr
umschließendes Rechteck und überprüfen über die Funktion CGRectIntersectsRect, ob sich
die beiden Rechtecke schneiden. Das umschließende Rechteck der Seite berechnet sich dabei
einfach aus einer vertikalen Verschiebung des Rechtecks in der Variablen thePageFrame. Listing 6.98 enthält den Code für diese Seitenfilterung.
for(int i = 0; i < theCount; ++i) {
thePageFrame.origin.y = i * theHeight;
if(CGRectIntersectsRect(inRect, thePageFrame)) {
...
}
}
Listing 6.98 Filterung der überlappenden Seiten
Innerhalb des if-Blocks muss die Methode den Rahmen und die PDF-Seite zeichnen. Damit
der Seitenrahmen nicht am Rand des Views klebt, berechnet sie über die Funktion CGRectInset ein Rechteck, dessen Ränder jeweils zehn Punkte Abstand zum Seitenrahmen haben.
Das Koordinatensystem eines Grafikkontexts hat seinen Ursprung unten links, während der
eines Views oben links liegt. Cocoa Touch spiegelt den Inhalt des Grafikkontexts, damit der
Grafikkontext genau auf dem View liegt. Dadurch sieht die Seite so wie in Abbildung 6.41 aus.
Diese Transformation veranschaulicht auch Abbildung 6.42. Der View spiegelt sich im Grafikkontext mit der Seite 1 an der horizontalen Achse, und dadurch entsteht die gespiegelte
Anzeige im View 2.
706
Abbildung 6.41 Bei Core Graphics steht die Welt kopf.
Vertikale Spiegelung
Der Grafikkontext stellt nicht nur PDF-Seiten horizontal gespiegelt dar, sondern auch Texte
und Bilder. Um eine nicht gespiegelte Darstellung dieser Objekte zu erhalten, können Sie das
im Folgenden beschriebene Vorgehen verwenden.
Diese Spiegelung lässt sich durch eine Skalierung, die das Vorzeichen der y-Achse umkehrt,
wieder aufheben, und Sie können sie durch den Funktionsaufruf CGContextScaleCTM(theContext, 1.0, –1.0); auf den Grafikkontext anwenden. Allerdings klappt diese Transformation
das Bild wieder um die Nulllinie und somit aus dem sichtbaren Bereich des Views hinaus 3.
Um das Bild wieder in die sichtbare Fläche des Views zu bekommen, müssen Sie es vertikal
verschieben. Das erreichen Sie durch eine weitere Transformation 4 oder durch eine Korrektur des Seitenrechtecks.
707
Models, Layer, Animationen
6.4
View
Spiegelung
1
2
Scrollviews und gekachelte Layer
Bei dieser Variante haben Sie gegenüber der Attributdeklaration im Interface-Block den Vorteil, dass Sie dieses Attribut vor einem Verwender der Klasse verstecken. Das ist sinnvoll, da
es ein Implementierungsdetail der Klasse ist.
Verschiebung
Spiegelung
6
4
3
View
Abbildung 6.42 Korrektur durch Spiegelung und Verschiebung
Diese Korrektur ist relativ einfach durchzuführen, wenn Sie die Seite im Kontext an die Stelle
zeichnen, wo der View liegt. In Abbildung 6.42 bedeutet das, dass Sie die erste Seite in das
Rechteck 1 zeichnen. Die Spiegelung von Cocoa Touch, die den Kontext auf den View abbildet, klappt die Seite dann nach oben, so dass sie auf dem gestrichelten Rechteck 2 liegt.
Wenn Sie dann den Kontext erneut spiegeln, landet der View genau an der Stelle – also Rechteck 3 –, wo er hingehört. Listing 6.99 implementiert diese Korrektur der Koordinaten.
CGPDFPageRef thePage = CGPDFDocumentGetPage( self.document, i + 1);
CGContextSaveGState(theContext);
CGContextSetGrayStrokeColor(theContext, 0.5, 1.0);
CGContextStrokeRect(theContext, CGRectInset( thePageFrame, 10.0, 10.0));
CGContextScaleCTM(theContext, 1.0, –1.0);
// Verschiebt die Seite auf die Koordinaten des Views
thePageFrame.origin.y = -(i + 1) * theHeight;
[self drawPage:thePage inRect:thePageFrame context:theContext];
CGContextRestoreGState(theContext);
Listing 6.99 Rahmen und Seite mit Korrektur der Spiegelung zeichnen
Außerdem stellt die Klasse entsprechende Accessoren für das Attribut zur Verfügung, und
die dealloc-Methode gibt das Dokument am Ende der Lebenszeit des Views frei:
- (void)dealloc {
CGPDFDocumentRelease(document);
}
- (CGPDFDocumentRef)document {
return document;
}
- (void)setDocument:(CGPDFDocumentRef)inDocument {
if(inDocument != document) {
CGPDFDocumentRelease(document);
document = inDocument;
CGPDFDocumentRetain(document);
}
}
Listing 6.101 »dealloc«-Methode und Accessoren der Klasse »PDFView«
Die Speicherverwaltung in Core Foundation
Die Typen und Funktionen, die mit CGPDFDocument beginnen, gehören zu dem Framework Core
Graphics, das ein C-Framework ist. Es basiert auf dem C-Framework Core Foundation. Beide
Frameworks enthalten keinen Objective-C-Code und greifen auch nicht auf Objective-C-Klassen zu. Core Foundation verwendet das manuelle Referenzzählen zur Speicherverwaltung,
und es hat drei Speicherverwaltungsregeln, die denen für Objective-C sehr stark ähneln:
왘 Sie halten das Objekt einer Referenz, wenn Sie die Referenz über eine Funktion mit dem
Damit haben Sie den komplizierten Teil der Implementierung geschafft. Der View speichert
das PDF-Dokument in dem Attribut document, das den Typ CGPDFDocumentRef hat. Die zu iOS 5
gehörende Objective-C-Version erlaubt auch, im Implementierungsblock der Klasse Attribute
zu deklarieren. Davon macht die Klasse PDFView Gebrauch, wie Sie in Listing 6.100 sehen:
@implementation PDFView {
CGPDFDocumentRef document;
}
...
@end
Namensbestandteil Create oder Copy erzeugen.
왘 Wenn Sie eine Referenz auf einem anderen Weg erhalten, halten Sie das Objekt nicht,
außer Sie rufen die Funktion CFRetain dafür auf.
왘 Für alle Referenzen, die ein Objekt halten, müssen Sie die Funktion CFRelease aufrufen,
um sie freizugeben.
Viele C-Datentypen haben eigene Funktionen für das Retain und Release. Deren Name beginnt
dann mit dem Namen des Typs. Beispielsweise heißen diese Funktionen CGPDFDocumentRetain
und CGPDFDocumentRelease für PDF-Dokumente. Im Gegensatz zu der Speicherverwaltung in
Objective-C bietet Core Foundation allerdings keinen Autoreleasepool.
Listing 6.100 Implementierungsblock mit Attributdeklaration
708
709
6
Models, Layer, Animationen
6.5
Da die PDF-Dokumente keine Objective-C-Klasse haben, implementiert der PDF-View die
Accessoren manuell.
Der View muss noch seine Layer-Klasse setzen und den Layer initialisieren. Dabei sollen die
Kacheln jeweils die volle Breite des Views haben, aber nur ein Viertel der sichtbaren Höhe.
Der volle View enthält alle Seiten des Dokuments, wobei allerdings jeweils nur eine Seite die
sichtbare Fläche vollständig bedecken kann. Um die Kachelhöhe zu erhalten, müssen Sie also
die Höhe des Views zuerst durch die Anzahl der Seiten teilen und danach durch 4. Ein guter
Ort für diese Berechnung ist die Methode layoutSubviews, da der View sie bei allen Größenänderungen aufruft. Die vollständige Verwaltung des Layers finden Sie in Listing 6.102.
+ (id)layerClass {
return [CATiledLayer class];
}
CGPDFDocumentRef theDocument = CGPDFDocumentCreateWithURL((__bridge CFURLRef)
theURL);
self.pdfView.document = theDocument;
CGPDFDocumentRelease(theDocument);
Listing 6.103 Laden eines PDF-Dokuments
Schließlich muss der Controller noch die Höhe des PDF-Views an das PDF-Dokument anpassen. Da jeweils eine Seite die angezeigte Fläche des Views einnehmen soll, multiplizieren Sie
für die Anpassung einfach die Höhe des Scrollviews mit der Anzahl der Seiten.
- (void)viewWillAppear:(BOOL)inAnimated {
[super viewWillAppear:inAnimated];
CGRect theFrame = self.pdfView.frame;
- (void)awakeFromNib {
[super awakeFromNib];
CATiledLayer *theLayer = (CATiledLayer *)self.layer;
theLayer.levelsOfDetail = 4;
theLayer.levelsOfDetailBias = 4;
}
Einführung in das Sprite-Kit
theFrame.size.height = CGRectGetHeight(self.scrollView.frame) *
self.pdfView.countOfPages;
self.scrollView.contentSize = theFrame.size;
self.pdfView.frame = theFrame;
}
Listing 6.104 Anpassung der View-Höhe an das Dokument
- (void)layoutSubviews {
[super layoutSubviews];
CATiledLayer *theLayer = (CATiledLayer *)self.layer;
CGSize theSize = self.bounds.size;
theSize.height /= 4 * self.countOfPages;
theLayer.tileSize = theSize;
}
6.5 Einführung in das Sprite-Kit
Mit dem Sprite-Kit stellt Apple ab iOS 7 ein Framework bereit, mit dem sich relativ einfach
2D-Spiele entwickeln lassen. Wie der Name bereits andeutet, bietet es eine einfache Möglichkeit, beliebig viele Sprites auf dem Bildschirm darzustellen. Sprites sind frei bewegliche oder
auch feste Objekte, die Figuren oder Gegenstände des Spiels darstellen und die die Grafikhardware sehr schnell und effizient anzeigen kann.
Listing 6.102 Die Layer-Verwaltung des PDF-Views
Damit der View auch tatsächlich ein Dokument anzeigt, müssen Sie ihm eine Referenz auf
ein Dokument übergeben, die Sie über die Funktion CGPDFDocumentCreateWithURL erzeugen
können. Der View erzeugt natürlich nicht selbst die Referenz, sondern der Controller. Im Beispielprogramm ist das die Klasse PDFViewController, die ein Beispieldokument aus dem Ressourcenordner des Programms lädt.
6.5.1 Sprites und Knoten
- (void)viewDidLoad {
[super viewDidLoad];
NSURL *theURL = [[NSBundle mainBundle] URLForResource:@"lorem-ipsum"
withExtension:@"pdf"];
Für die Modellierung von pixelbasierten Sprites gibt es die Klasse SKSpriteNode, die ein Sprite
auf Basis einer Textur darstellt. Eine Textur ist eine Pixelgrafik, die sich für die effiziente und
schnelle Darstellung von Spielen eignet. Der wichtigste Unterschied zu den anderen Pixelgrafiken, die Sie im Verlauf dieses Buches bereits kennengelernt haben, ist, dass Cocoa Touch
710
Neben der Darstellung von pixelbasierten Sprites erlaubt das Sprite-Kit auch einfache, geometrische Formen, Texte, Partikelquellen, Videos und 3D-Szenen analog zu Sprites zu verwenden. Die Verallgemeinerung dieser Objekte bezeichnet das Sprite-Kit als Knoten mit der
gemeinsamen Basisklasse SKNode.
711
6
Models, Layer, Animationen
6.5
Einführung in das Sprite-Kit
die Bilddaten einer Textur im Video- und nicht im Hauptspeicher des Gerätes ablegt. Am einfachsten können Sie ein Sprite aus einem Bild beziehungsweise einem Image-Asset aus dem
Ressourcenordner der App und dem Convenience-Konstruktor spriteNodeWithImageNamed:
erzeugen:
Die Eltern-Kind-Beziehungen von Knoten sind analog zu den Superview- und Subview-
SKNode *theSprite = [SKSpriteNode spriteNodeWithImageNamed:@"witch"];
SKNode *theChild = ...;
SKNode *theParent = ...;
Listing 6.105 Ein Sprite aus einem Bild erzeugen
Geometrische Formen können Sie über Knoten der Klasse SKShapeNode darstellen; dabei
beschreiben Sie den Rand der Figur über einen Grafikpfad. Außerdem können Sie die Rahmen- und Füllfarbe sowie die Breite des Rahmens und einen Glüheffekt festlegen.
SKShapeNode *theBrick = [SKShapeNode new];
CGRect theBounds = CGRectMake(-20.0, -10.0, 40.0, 20.0);
CGPathRef thePath = CGPathCreateWithRect(theBounds, NULL);
theBrick.path = thePath;
theBrick.fillColor = [SKColor redColor];
theBrick.strokeColor = [SKColor whiteColor];
theBrick.glowWidth = 1.0;
Beziehungen beziehungsweise Superlayer- und Sublayer-Beziehungen, die Sie bereits kennengelernt haben. Zusätzlich können Sie jedem Knoten einen Namen geben und ihn über
die Methode childNodeWithName: von seinem Elternknoten aus wiederfinden.
theChild.name = @"Hero";
[theParent addChild:theChild];
...
if(theChild == [theParent childNodeWithName:@"Hero"]) {
...
}
Listing 6.107 Kindknoten über seinen Namen finden
Sofern der Knoten theParent nur einen Kindknoten mit Namen @"Hero" hat, ist die If-Bedingung in Listing 6.107 wahr. Diese Methode liefert immer das erste Kind mit dem angegebenen Namen. Es dürfen jedoch auch mehrere Kinder den gleichen Namen verwenden, und in
diesem Fall können Sie mit der Methode enumerateChildNodesWithName:usingBlock: über die
Listing 6.106 Knoten mit einem Rechteck erzeugen
Wanderer zwischen den Welten
Das Sprite-Kit unter iOS verwendet für die Darstellung von Farben Objekte der Klasse UIColor,
auch wenn Listing 6.106 den Bezeichner SKColor anstatt UIColor für die Konstruktion einsetzt.
Dieser Bezeichner ist ein Makro, das das Sprite-Kit unter iOS als UIColor und unter OS X als
NSColor definiert.
Knoten besitzen viele Eigenschaften, die Sie bereits von Views und Layern her kennen, und
so finden Sie auch in dieser Klasse die Propertys frame, alpha und hidden. Die Position des
Knotens legen Sie über die Property position fest, da Sie den Wert der Property frame nicht
explizit verändern können. Für die geometrische Transformation eines Knotens stehen die
Propertys xScale, yScale und zRotation zur Verfügung, die eine Stauchung oder Streckung
beziehungsweise Rotation des Knotens erlauben.
Die Position geben Sie immer relativ zum Elternknoten an, den Sie über die Property parent
erhalten. Umgekehrt erhalten Sie alle Kinder eines Knotens über dessen Property children,
und über addChild: beziehungsweise insertChild:atIndex: können Sie zu einem Knoten ein
neues Kind hinzufügen. Über die Methode removeFromParent können Sie ein Kind von seinem
Elternknoten trennen, und mit removeAllChildren entfernen Sie alle Kinder eines Knotens.
712
entsprechenden Knoten iterieren.
[theParent enumerateChildNodesWithName:@"Goblin" usingBlock:
^(SKNode *inGoblin, BOOL *outStop) {
...
if(inGoblin.hidden) {
*outStop = YES;
}
}];
Listing 6.108 Alle Kindknoten mit dem gleichen Namen ansprechen
Während Sie über den ersten Parameter des Blocks jeweils den Knoten erhalten, können Sie
über den zweiten Parameter die Iteration stoppen, indem Sie das Ziel des Zeigers auf YES setzen. In Listing 6.108 hält beispielsweise die Iteration an, sobald sie einen versteckten Knoten
besucht hat.
Sie können über childNodeWithName: und enumerateChildNodesWithName:usingBlock: nicht
nur direkte Unterknoten ansprechen, sondern auch tiefer und sogar höher liegende Knoten
finden. Die Syntax ähnelt dabei sehr stark der Syntax von Dateipfaden oder XPath. Die möglichen Bestandteile eines Knotenpfads stellt Tabelle 6.9 dar:
713
6
Models, Layer, Animationen
6.5
Einführung in das Sprite-Kit
Element
Beschreibung
Projektinformation
/
Trennt die Knotenebenen im Pfad voneinander; wenn es am
Anfang eines Pfades steht, kennzeichnet es die Wurzel.
Den Quellcode des Beispielprojekts finden Sie im Github-Repository zum Buch im Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Ausgabe_iOS8/Apps/iOS7/Games.
..
Sucht eine Ebene über der aktuellen.
//
Durchsucht rekursiv alle Unterknoten.
*
Steht für beliebige Zeichen eines Knotennamens.
[Zeichenmenge]
Passt auf alle Zeichen aus der Zeichenmenge. Die Zeichenmenge
beschreiben Sie durch eine durch Komma oder Minuszeichen
getrennte Liste von Zeichen (z. B. [a-z], [0-9], [0,2,4,6,8]).
Zeichenfolge aus
Buchstaben und Zahlen
Passt auf Knotennamen mit genau dieser Zeichenfolge.
Als Beispielprojekt für die Verwendung des Sprite-Kits soll eine einfache Version des bekannten Spiels Breakout4 dienen. Damit Sie das Sprite-Kit in einem Projekt nutzen können, müssen Sie zuerst das SpriteKit.framework zu dem Projekt hinzufügen, wozu Sie in den TargetEinstellungen den Plus-Button in der Rubrik Linked Frameworks and Libraries drücken.
In dem erscheinenden Dialog wählen Sie den entsprechenden Eintrag aus und drücken
danach den Button Add (siehe Abbildung 6.43).
Tabelle 6.9 Die Elemente eines Knotenpfades
Beispielsweise können Sie über /ball alle Knoten auf der obersten Ebene finden, die den
Namen ball haben. Mit dem Ausdruck //robot/.. finden Sie alle Knoten, die einen Kindknoten namens robot haben, oder anders ausgedrückt: alle Elternknoten von Knoten mit dem
Namen robot, und der Ausdruck */goblin liefert alle Enkelknoten mit dem Namen goblin.
Über die Property userData können Sie zu jedem Knoten beliebige Daten in einem veränderlichen Dictionary speichern, wozu Sie allerdings zuerst der Property ein Dictionary zuweisen
müssen. Die Property userData eignet sich somit sehr gut dazu, zusätzliche Eigenschaften
abzulegen. Das können beispielsweise Lebenspunkte oder gesammelte Gegenstände der
entsprechenden Spielfigur sein.
theHero.userData = [NSMutableDictionary new];
theHero.userData[@"armor"] = @100;
theHero.userData[@"health"] = @400;
theHero.userData[@"weapons"] = @[];
Abbildung 6.43 Das SpriteKit.framework zum Projekt hinzufügen
Listing 6.109 Benutzerdefinierte Eigenschaften an einem Knoten speichern
6.5.2 Mach' mir eine Szene
Um die Knoten auf einem iOS-Gerät anzeigen zu können, müssen Sie diese in eine Szene einfügen, die Sie wiederum einem View der Klasse SKView für die Darstellung zuordnen können. Sie
erstellen eine Szene, indem Sie eine Unterklasse der Klasse SKScene anlegen und darin Methoden überschreiben. Da die Klasse SKScene eine Unterklasse von SKNode ist, können Sie über die
oben beschriebenen Methoden Knoten zu einer Szene hinzufügen, suchen und entfernen.
714
Für die Darstellung einer Sprite-Kit-Szene benötigen Sie einen View der Klasse SKView. Dazu
können Sie im Storyboard einen View anlegen und in dessen Identitätsinspektor die Klasse
auf SKView ändern. Das geht auch mit dem View eines Viewcontrollers wie beispielsweise
beim Breakout-Viewcontroller des Beispielprojekts. Dafür ziehen Sie ein neues Viewcontroller-Objekt aus der Bibliothek auf die Zeichenfläche, wählen dessen View aus und ändern dessen Klasse über den Identitätsinspektor.
4 http://de.wikipedia.org/wiki/Breakout_(Computerspiel)
715
6
Models, Layer, Animationen
6.5
Der Viewcontroller initialisiert die Szene über die Methode initWithSize: und weist sie dem
View über die Methode presentScene: zu. Das Beispielprojekt erzeugt die Szene in der
Methode viewDidAppear:, weil der View zum Aufrufzeitpunkt dieser Methode die endgültige
Größe hat. Die Implementierung der Sprite-Kit-Szene erfolgt über die Klasse BreakoutScene,
und die beschriebene Initialisierung sehen Sie in Listing 6.110:
CGPoint thePoint = CGPointMake(theBrickSize.width / 2.0, theSize.height (i + 0.5) * theBrickSize.height);
theMinimalY = thePoint.y;
for(NSUInteger j = 0; j < kBricksPerRow; ++j) {
NSUInteger theIndex = drand48() * theCount;
SKColor *theColor = theColors[theIndex];
SKNode *theBrick = [self brickWithColor:theColor size:theBrickSize
position:thePoint];
- (void)viewDidAppear:(BOOL)inAnimated {
[super viewDidAppear:inAnimated];
SKView *theView = (SKView *)self.view;
BreakoutScene *theScene = [BreakoutScene sceneWithSize: theView.bounds.size];
theBrick.userData[kPoints] = @((theIndex + 1) * 10);
theBrick.userData[kImpulse] = @(10 + theIndex * 3);
[self addChild:theBrick];
thePoint.x += theBrickSize.width;
theScene.scaleMode = SKSceneScaleModeAspectFill;
[theView presentScene:theScene];
self.scene = theScene;
}
}
Listing 6.110 Initialisierung der Szene
Die Klasse BreakoutScene besitzt die Methode buildBricks, die die Steinreihen am oberen
Spielfeldrand erzeugt. Es gibt vier unterschiedliche Arten von Steinen, die sich in der Farbe,
ihrem Punktwert und dem Impulsfaktor unterscheiden. Die Impulsfaktoren verwendet die
Szene, um den Ball unterschiedlich stark von den Steinen abprallen zu lassen. Den Punktwert und den Impulsfaktor legt die Methode dabei in den Nutzerdaten ab. Da der Koordinatenursprung der Szene unten links liegt, berechnet die Methode die y-Position der Steine
relativ zur Höhe der Szene.
- (CGSize)brickSize {
CGSize theSize = self.size;
return CGSizeMake(theSize.width / kBricksPerRow, theSize.width / (3 *
kBricksPerRow));
}
}
Listing 6.111 Die Steinreihen für das Spiel erstellen
Wie Sie in Listing 6.111 sehen können, erfolgt die Erzeugung der Knoten in den Methoden
brickWithColor:size:position: und rectangleWithColor:size:position:, die Sie in Listing 6.112 finden. Dabei erzeugt diese Methode das Rechteck des Steins zentriert zum
angegebenen Punkt. Außerdem weist sie allen Steinen einen einheitlichen Namen aus
der Konstanten kBrick zu.
- (SKNode *)brickWithColor:(SKColor *)inColor size:(CGSize)inSize position:
(CGPoint)inCenter {
SKNode *theBrick = [self rectangleWithColor:inColor size:inSize position:
inCenter];
}
theBrick.name = kBrick;
return theBrick;
- (void)buildBricks {
CGSize theSize = self.size;
CGSize theBrickSize = self.brickSize;
NSArray *theColors = @[ [UIColor redColor], [UIColor orangeColor],
[UIColor yellowColor], [UIColor greenColor] ];
NSUInteger theCount = [theColors count];
CGFloat theMinimalY = 0.0;
[self removeAllBricks];
for(NSUInteger i = 0; i < kRows; ++i) {
716
Einführung in das Sprite-Kit
}
- (SKNode *)rectangleWithColor:(SKColor *)inColor size:(CGSize)inSize position:
(CGPoint)inCenter {
SKShapeNode *theRectangle = [SKShapeNode new];
CGRect theBounds = CGRectMake(-inSize.width / 2.0, -inSize.height / 2.0,
inSize.width, inSize.height);
CGPathRef thePath = CGPathCreateWithRect(theBounds, NULL);
theRectangle.path = thePath;
717
6
Models, Layer, Animationen
theRectangle.fillColor = inColor;
theRectangle.strokeColor = [SKColor whiteColor];
theRectangle.glowWidth = 0.0;
theRectangle.position = inCenter;
theRectangle.userData = [NSMutableDictionary new];
CGPathRelease(thePath);
return theRectangle;
6.5
Einführung in das Sprite-Kit
}
return self;
}
- (SKNode *)racketWithSize:(CGSize)inSize position:(CGPoint)inCenter {
SKNode *theRacket = [self rectangleWithColor:[SKColor whiteColor] size:inSize
position:inCenter];
}
theRacket.name = @"racket";
return theRacket;
Listing 6.112 Erzeugung eines Steins
Diesen Namen nutzt die Methode removeAllBricks dazu, alle Steine zu löschen. Das ist beispielsweise notwendig, wenn Sie ein neues Spiel beginnen und die Szene dafür das Spielfeld
neu aufbauen muss. Die Methode enumerateChildNodesWithName:usingBlock: lässt sich hierbei leider nicht einsetzen, da sie über das Array der Kindknoten iteriert, und währenddessen
darf der Block dieses Array nicht verändern; also insbesondere keine Knoten entfernen.
- (void)removeAllBricks {
NSPredicate *thePredicate = [NSPredicate predicateWithFormat:@"name = %@",
kBrick];
NSArray *theNodes = [self.children filteredArrayUsingPredicate:thePredicate];
[theNodes makeObjectsPerformSelector:@selector(removeFromParent)];
}
}
Listing 6.114 Initialisierung der Szene
Die start-Methode (siehe Listing 6.115) erzeugt schließlich noch in der Mitte des Spiels den
Ball über die Methode ballWithRadius:position:, mit dem der Spieler die Steine am oberen
Spielfeldrand abschießen kann. Außerdem setzt sie die Property isRunning auf YES. Diese
Property können andere Objekte wie beispielsweise der Viewcontroller beobachten, um
Zustandsänderungen des Spiels mitzubekommen.
- (SKNode *)ballWithRadius:(CGFloat)inRadius position:(CGPoint)inCenter {
SKShapeNode *theBall = [SKShapeNode new];
CGRect theBounds = CGRectMake(-inRadius, -inRadius, 2 * inRadius, 2 * inRadius);
CGPathRef thePath = CGPathCreateWithEllipseInRect(theBounds, NULL);
Listing 6.113 Alle Steine aus der Szene entfernen
theBall.name = kBall;
theBall.path = thePath;
theBall.fillColor = [SKColor yellowColor];
theBall.strokeColor = theBall.fillColor;
theBall.glowWidth = 1.0;
theBall.position = inCenter;
CGPathRelease(thePath);
return theBall;
Die Klasse BreakoutScene überschreibt die Methode initWithSize: und ruft von dort die
Methode buildBricks aus. Außerdem legt sie über die Methode racketWithSize:position:
noch einen Knoten für den Schläger an, und startet das Spiel über die Methode start.
- (id)initWithSize:(CGSize)inSize {
self = [super initWithSize:inSize];
if(self) {
CGSize theBricklSize = self.brickSize;
CGSize theRacketSize = CGSizeMake(theBricklSize.width, theBricklSize.height /
2.0);
self.backgroundColor = [SKColor blueColor];
[self buildBricks];
self.racket = [self racketWithSize:theRacketSize position:
CGPointMake(inSize.width / 2.0, theBricklSize.height)];
[self addChild:self.racket];
[self start];
718
}
- (void)start {
if(self.ball == nil) {
CGRect theFrame = self.frame;
CGFloat theRadius = CGRectGetWidth(theFrame) / (6 * kBricksPerRow);
CGPoint thePosition = CGPointMake(CGRectGetMidX(theFrame),
CGRectGetMidY(theFrame));
self.ball = [self ballWithRadius:theRadius position:thePosition];
719
6
Models, Layer, Animationen
6.5
Einführung in das Sprite-Kit
[self addChild:self.ball];
self.score = 0;
self.isRunning = YES;
}
}
Szene
darstellen
Knoten
aktualisieren
update:
Listing 6.115 Erzeugung des Balls und Starten eines neuen Spiels
Damit haben Sie alle Methoden für die Erzeugung der Elemente auf dem Spielfeld kennengelernt, und wenn Sie die App damit starten, sehen Sie auf dem Bildschirm ein Spielfeld, wie es
Abbildung 6.44 zeigt. Das sieht ja schon sehr stark nach einem einfachen Breakout-Spiel aus;
allerdings fehlt der Szene noch ein wichtiges Merkmal: die Bewegung der enthaltenen
Objekte.
didSimulatePhysics
physikalische
Effekte
simulieren
Aktionen
ausführen
didEvaluateActions
Abbildung 6.45 Aktualisierungszyklus von Sprit-Kit-Szenen
Die Schleife beginnt mit dem Aufruf der Methode update: der Szene. Sie können diese
Methode überschreiben, um in der Szene Aktualisierungen auszuführen. Diese Aktualisierungen können Sie sehr häufig über Aktionen auf den Knoten beschreiben, die die Szene im
nächsten Schritt ausführt. Da die Ausführung einer Aktion in der Regel mehrere Schleifendurchläufe beanspruchen kann, führt das Sprite-Kit bei jedem Durchlauf gegebenenfalls nur
einen entsprechenden Teil der Aktion aus. Nach der Ausführung der Aktionen ruft die Szene
die Methode didEvaluateActions auf, die Sie ebenfalls überschreiben können.
Das Sprite-Kit verfügt über eine Physik-Engine, die mit den Knoten eine Simulation physikalischer Effekte erlaubt. Nach der Ausführung der Aktionen kommt die Physik-Engine zum
Zuge und berechnet die physikalischen Auswirkungen der Aktionen auf die Knoten. Den
Abschluss dieser Simulation kennzeichnet der Aufruf der Methode didSimulatePhysics.
Auch diese Methode dürfen Sie in Ihren Unterklassen von SKScene überschreiben. Am Ende
des Schleifendurchlaufs stellt der View den aktuellen Zustand der Szene dar.
Die Bildrate
Abbildung 6.44 Das Breakout-Spiel im Simulator
6.5.3 ...uuuund Äkschion
Eine Szene übernimmt jedoch nicht nur die Darstellung der enthaltenen Knoten, sondern
enthält auch die Logik für die Spielsteuerung. Diese Logik basiert auf einer Programmschleife, wie sie Abbildung 6.45 schematisch darstellt.
720
Jedes Einzelbild (Frame) benötigt somit einen kompletten Durchlauf dieser Schleife. Die
Anzahl der Bilder, die ein Spiel pro Sekunde (Bildrate, Frames per Second oder kurz fps) zeichnet, verwendet man gerne als Maßeinheit für die Leistungsfähigkeit eines Spiels, da eine
höhere Bildrate in der Regel einen flüssigeren Spielablauf impliziert. Sie können sich die Bildrate sehr leicht in einem SKView anzeigen lassen, indem Sie dessen Property showsFPS auf YES
setzen. Der View blendet dann am unteren rechten Rand die Bildrate ein.
Die Bildrate bei dem Breakout-Spiel ist jedoch kein fester Wert, und schwankt sogar ohne die
Verwendung von Aktionen und Animationen in der letzten Nachkommastelle. Sie können
schon daran sehen, dass der View die Szene tatsächlich mehrmals pro Sekunde darstellt. Die
tatsächlich erreichbare Bildrate hängt einerseits von der Komplexität der Szene und andererseits der Leistungsfähigkeit der Hardware ab.
721
6
Models, Layer, Animationen
6.5
Wie bereits erwähnt, führen Sie Änderungen an einer Szene in der Regel über Aktionen
durch, die Sie über die Klasse SKAction erzeugen. Über Aktionen können Sie sowohl Knoten
in der Szene animieren als auch diskrete Änderungen ausführen.
Mit Hilfe von Aktionen lässt sich beispielsweise die Steuerung des Schlägers umsetzen. Dazu
führt das Spiel jeweils eine Aktion aus, die die horizontale Position des Schlägers ändert. Als
Position verwendet es dabei die aktuelle Fingerposition des Spielers, die sich über die gleichen
Methoden wie bei einem View ermitteln lassen, da auch die Klasse SKNode eine Unterklasse
von UIResponder ist. Die Klasse BreakoutScene überschreibt die Methode touchesMoved:withEvent:, um die Fingerposition zu ermitteln. Die Implementierung übergibt die Touch-Objekte
an die Methode updateRacketWithTouches:, die die Schlägerposition entsprechend aktualisiert.
- (void)touchesMoved:(NSSet *)inTouches withEvent:(UIEvent *)inEvent {
[self updateRacketWithTouches:inTouches];
}
- (void)updateRacketWithTouches:(NSSet *)inTouches {
SKNode *theRacket = self.racket;
UITouch *theTouch = [inTouches anyObject];
CGPoint thePoint = [theTouch locationInNode:self];
SKAction *theAction = [SKAction moveToX:thePoint.x duration:0.0];
[theRacket runAction:theAction];
}
Listing 6.116 Aktualisierung der Schlägerposition
Durch eine längere Dauer können Sie den Schläger explizit animieren, was zu einer langsameren Schlägerbewegung führt. Der Schläger scheint damit dem Finger nachzulaufen. Das können Sie beispielsweise als zusätzliche Schwierigkeitsstufe in dem Spiel einsetzen.
Für das Verschieben von Knoten bietet die Klasse SKAction noch eine Reihe weiterer Methoden an. Beispielsweise können Sie über moveTo:duration: einen Knoten an einen bestimmten Punkt oder über moveByX:y:duration: um eine bestimmte Distanz verschieben. Dabei
bewegt das Sprite-Kit den Knoten entlang einer geraden Linie vom aktuellen Punkt zum Ziel.
Die Methode followPath:asOffset:orientToPath:duration: erlaubt hingegen, Knoten entlang
eines beliebigen Core-Graphics-Pfades zu bewegen, wobei Sie über den Parameter orientToPath festlegen können, dass sich der Knoten auch noch entsprechend der Pfadbewegung dreht.
Dadurch ist es beispielsweise sehr leicht möglich, ein Flugzeug in einem Spiel einen Looping
fliegen zu lassen; dafür brauchen Sie nur die Flugkurve als Core-Graphics-Path beschreiben.
Über Aktionen können Sie natürlich noch andere Eigenschaften der Knoten verändern. Dazu
gehören die Drehung, die Größe, die Skalierung, die Farbe, die Textur und die Geschwindigkeit. Außerdem können Sie über Aktionen Knoten löschen, einen Ton abspielen oder beliebigen Programmcode über Blöcke ausführen. Wenn das Spiel stoppt, entfernt die Szene
den Ball über die Aktion removeFromParent aus dem Spielfeld und zeigt einen Knoten mit
dem Text »Game Over« an. Dabei blendet sie den Textknoten über die Aktion fadeInWithDuration: ein. Den entsprechenden Programmcode dazu sehen Sie in Listing 6.117.
- (void)stop {
SKNode *theBall = self.ball;
self.ball = nil;
if(theBall != nil) {
SKLabelNode *theNode = [SKLabelNode labelNodeWithFontNamed:
@"Helvetica Bold"];
CGRect theFrame = self.frame;
Diese Methode ermittelt aus den Touch-Objekten eine Fingerposition und verschiebt den
Schläger entsprechend entlang der x-Achse. Dazu erzeugt sie über den Convenience-Konstruktor moveToX:duration: eine Aktion, die der Schlägerknoten über die Methode runAction:
ausführt. Durch diese Änderung können Sie den Schläger horizontal auf dem Spielfeld verschieben.
[theBall runAction:[SKAction removeFromParent]];
theNode.name = kGameOverLabel;
theNode.text = NSLocalizedString(@"Game Over", @"Game Over");
theNode.position = CGPointMake(CGRectGetMidX(theFrame),
CGRectGetMidY(theFrame));
theNode.alpha = 0.0;
[self addChild:theNode];
[theNode runAction:[SKAction fadeInWithDuration:0.25]];
self.isRunning = NO;
Der zweite Parameter der Methode moveToX:duration: gibt die Dauer der Aktionsausführung
an. Wenn Sie hier einen Wert größer als 0 verwenden, animiert das Sprite-Kit diese Änderung. Mit einem Wert von 0 folgt der Schläger also nahezu ohne Verzögerung dem Finger.
Nutzereingaben und Animationen
Obwohl das Spiel auf diese Weise die Bewegung des Schlägers nicht explizit animiert, bewegt
er sich dennoch flüssig und nicht sprunghaft über das Spielfeld, weil Cocoa Touch die Methode
touchesMoved:withEvent: bei jeder Bewegung mehrmals aufruft. Dadurch entsteht eine
implizite Animation des Schlägers. Wenn Sie also wollen, dass die Knoten unmittelbar auf die
Gesten des Nutzers reagieren, sollten Sie einerseits auf explizit animierte Bewegungen verzichten und die Gesten möglichst kleinteilig auswerten.
722
Einführung in das Sprite-Kit
}
}
Listing 6.117 Spiel beenden
723
6
Models, Layer, Animationen
6.5
Möchten Sie eine Aktion kontinuierlich wiederholend ausführen lassen, können Sie über
repeatAction:count: oder repeatActionForever: mit dieser Aktion als Parameter eine neue
erzeugen, um die Aktion mit einer beziehungsweise unbegrenzten festen Anzahl von Wiederholungen auszuführen. Mit group: lassen sich mehrere Aktionen zu einer parallelen Ausführungsgruppe und mit sequence: zu einer Sequenz von aufeinanderfolgenden Aktionen
zusammenfassen. Um beispielsweise den Ball im Beispielprojekt blinken zu lassen, können
Sie die entsprechende Aktion wie in Listing 6.118 aufbauen. Die Sequenz blendet den Knoten
durch eine Animation zunächst aus und danach wieder ein, und durch repeatActionForever:
führt das Sprite-Kit die Aktion beliebig oft aus.
SKAction *theSequence = [SKAction sequence:@[ [SKAction fadeOutWithDuration:0.25],
[SKAction fadeInWithDuration:0.25] ]];
[self.ball runAction:[SKAction repeatActionForever:theSequence] withKey:@"blink"];
Listing 6.118 Aktionssequenz wiederholt ausführen
Der Code fügt die Aktion mit dem Schlüssel @"blink" zum Aktionsverzeichnis des Knotens hinzu, wodurch sich diese Aktion durch den Aufruf [self.ball removeActionForKey:@"blink"]; später wieder entfernen lässt. Bei der sequentiellen Ausführung von
Aktionen benötigt man häufig Pausen zwischen den einzelnen Aktionen, die sich über waitForDuration: und waitForDuration:range: erzeugen lassen. Während die erste Methode Pausen mit exakt der angegebenen Länge erzeugt, erstellt die zweite Methode Pausen mit einer
pseudo-zufälligen Länge aus einem Bereich. Dabei gibt der erste Parameter den Mittelpunkt
des Zeitbereiches und der Parameter range dessen Breite an. Geben Sie beispielsweise 1.0 und
0.5 als Parameterwerte an, so liegen die Pausenlängen zwischen 0.75 und 1.25. Beispielsweise
können Sie durch eine Pause die Dauer des sichtbaren Balls verlängern (siehe Listing 6.119).
SKAction *theSequence = [SKAction sequence:@[ [SKAction waitForDuration:0.25],
[SKAction fadeOutWithDuration:0.25], [SKAction fadeInWithDuration:0.25] ]];
[self.ball runAction:[SKAction repeatActionForever:theSequence] withKey:@"blink"];
Listing 6.119 Verwendung von Pausen
Über den View können Sie die Ausführung aller Aktionen und physikalischen Effekte in der
Szene anhalten lassen, indem Sie dessen Property paused auf YES setzen. Dadurch können Sie
beispielsweise das Spiel pausieren lassen, wenn der Nutzer über den Tabbarcontroller einen
anderen Viewcontroller auswählt. Sobald er zum Breakout-Spiel zurückkehrt, kann dessen
Viewcontroller die Pause beenden, und der Nutzer kann das unterbrochene Spiel an der
alten Position fortsetzen.
- (void)viewDidAppear:(BOOL)inAnimated {
[super viewDidAppear:inAnimated];
724
Einführung in das Sprite-Kit
if(self.scene) {
self.scene.paused = NO;
}
else {
SKView *theView = (SKView *)self.view;
BreakoutScene *theScene = [BreakoutScene sceneWithSize:theView.bounds.size];
theScene.scaleMode = SKSceneScaleModeAspectFill;
[theView presentScene:theScene];
self.scene = theScene;
}
}
- (void)viewWillDisappear:(BOOL)inAnimated {
[super viewWillDisappear:inAnimated];
self.scene.paused = YES;
}
Listing 6.120 Pausieren der Szene
6.5.4 Physik für alle
Natürlich können Sie für die Animation des Balls ebenfalls Aktionen verwenden. Allerdings
stoßen Sie mit diesem Ansatz sehr schnell auf Probleme, wie zum Beispiel:
왘 Die Berechnung von Kollisionen des Balls mit den Wänden, den Steinen oder dem Schlä-
ger ist für Aktionen nicht vorgesehen. Sie müssten diese Punkte und auch die Abprallwinkel selbst bestimmen. Das ist sogar für so ein einfaches Spiel wie Breakout recht
aufwändig.
왘 Bei einer Ballbewegung ist das Ziel des Balls zu Beginn nicht unbedingt bekannt. Der Ball
kann beispielsweise den Schläger treffen oder ins Aus gehen.
왘 Eine lineare Ballbewegung wirkt unter Umständen recht ungeschmeidig. Ein Beschleuni-
gungseffekt passt hier besser.
Bei komplexeren Spielen können unter Umständen noch weitere Probleme auftreten, und
so hat sich bei vielen Spielen der Einsatz einer Physik-Engine bewährt. Das Sprite-Kit verfügt
über eine Physik-Engine, die Simulationen von Effekten der Newtonschen Mechanik und
Partikelsystemen erlaubt. Beim Einsatz einer Physik-Engine weisen Sie jedem Objekt des
Spiels physikalische Eigenschaften wie beispielsweise eine Masse, eine Dichte und eine Fläche5 zu und wenden darauf physikalische Größen wie eine Kraft oder einen Impuls an,
wodurch sich der Zustand des Objekts, z. B. seine Position, seine Ausrichtung oder seine
Geschwindigkeit, verändert.
5 Das Sprite-Kit unterstützt nur zweidimensionale Spiele.
725
6
Models, Layer, Animationen
Das hört sich alles recht kompliziert an. Aber keine Sorge, die Verwendung der Physik-Engine
ist einfacher, als es sich anhört und es macht sogar Spaß.
Die wunderbare Welt der Schwerkraft
Wenn Sie die Physik-Engine verwenden wollen, sollten Sie sich zuerst Gedanken über die
Welt machen; natürlich nicht über die reale, sondern die Welt Ihres Spiels. Dabei sollten Sie
sich fragen, wie die Schwerkraft, auch Gravitation genannt, in Ihrem Spiel wirkt. Sollen
Gegenstände beispielsweise nach unten fallen? Oder hat die Schwerkraft keinen Einfluss,
weil der Spielende die Welt von oben sieht? Vielleicht wollen Sie ja auch ein etwas ausgefalleneres Spiel entwickeln, wo die Schwerkraft seitlich wirkt (z. B. wegen eines Staubsaugers
oder starken Magnets) oder nach oben (z. B. für Heliumballons oder die Welt steht Kopf). Es
gibt da viele Möglichkeiten.
Im Sprite-Kit modellieren Sie die Schwerkraft über ein Objekt der Klasse SKPhysicsWorld, auf das
Sie über die Property physicsWorld von der Szene aus zugreifen können. Die Richtung und die
Stärke der Schwerkraft weisen Sie diesem Objekt über die Property gravity über einen Vektor
des Typs CGVector zu. Standardmäßig verwendet das Sprite-Kit die Erdanziehung als Schwerkraft, die dem Vektor (0.0, -9.8) entspricht, und die auch das Breakout-Spiel verwendet.
Physikalische Größen und Maßeinheiten
Apple hat den Wert –9,8 nicht willkürlich gewählt, er entspricht ungefähr dem Wert der Erdbeschleunigung g6 von ca. 9,81 m/s². Für die Physik-Engine des Sprite-Kits sind die absoluten
Massen jedoch in der Regel irrelevant und nur die relativen Größen wichtig. Es macht also beispielsweise keinen Unterschied, ob zwei aufeinanderprallende Kugeln die Gewichte 1g und 3g,
5kg und 15kg oder 2t und 6t haben. Entscheidend ist nur das Verhältnis der beiden Gewichte
von 1 : 3; die Aufprallsimulation sieht in allen Fällen gleich aus.
Andererseits erleichtert die Verwendung realer Werte jedoch die Einhaltung von Verhältnissen
erheblich, auch wenn sich zugegebenermaßen die exakte Bestimmung des realen Gewichts
eines Stollentrolls oder eines Sternenkreuzers schwierig gestalten könnte. Dennoch garantieren auch die realistischsten Werte für alle physikalischen Größen keine perfekte Simulation
oder ein gut spielbares Spiel. Dafür ist in der Regel eine Feinabstimmung notwendig, bei der
Sie die optimalen Werte durch Ausprobieren ermitteln.
Wenn Sie lieber eine andere Schwerkraft verwenden möchten, können Sie über die Funktion
CGVectorMake einen entsprechenden Vektor erzeugen und über die Property zuweisen. Beispielsweise können Sie folgende Zeile in der Initialisierungsmethode der Szene verwenden,
um die Schwerkraft auszuschalten:
self.physicsWorld.gravity = CGVectorMake(0.0, 0.0);
6.5
Vektoren begegnen Ihnen bei der Verwendung des Sprite-Kits an vielen Stellen, z. B. bei der
Beschreibung von Kräften, Beschleunigungen oder Impulsen, und gelegentlich benötigen
Sie den absoluten Wert ohne die Richtung. Dieser entspricht der Länge des Vektors, die Sie
über den Satz des Pythagoras7 bestimmen können. Das Beispielprojekt verwendet dafür die
folgende Funktion:
static inline CGFloat CGVectorLength(CGVector inVector) {
return sqrtf(inVector.dx * inVector.dx + inVector.dy * inVector.dy);
}
Listing 6.122 Längenbestimmung für Vektoren
Modellierung physikalischer Körper
Für alle Knoten, die die Physik-Engine für die Simulation verwenden soll, müssen Sie die
physikalischen Eigenschaften modellieren. Dazu legen Sie jeweils ein Objekt der Klasse
SKPhysicsBody an, das Sie dem Knoten über die Property physicsBody zuweisen.
Im Breakout-Spiel soll die Physik-Engine die Flugbahn des Balls bestimmen, die im Wesentlichen der Abprall von dem Schläger, den Wänden und den Steinen beeinflusst. Obwohl die
Physik-Engine diese Objekte nicht bewegen soll, müssen Sie sie dennoch mit einem physikalischen Körper für die Kollisions- und Abprallberechnung versehen.
Über die Property dynamic können Sie verhindern, dass die Physik-Engine den zugehörigen
Knoten bewegt. Da jedes Rechteck im Spiel die Ballbewegung beeinflussen kann, erzeugt die
Methode rectangleWithColor:size:position: einen physikalischen Körper zu dem Rechteck
über den Convenience-Konstruktor bodyWithRectangleOfSize:. Bei der Konstruktion müssen
Sie die Form des Körpers beschreiben. Hierbei sind Sie gegenüber Knoten etwas eingeschränkt, da die Physik-Engine nur Kreise, Rechtecke und beliebige Polygone verarbeiten
kann. Falls Sie damit die Körperform nicht genau beschreiben können, sollten Sie eine möglichst einfache Näherung verwenden.
Körperannäherungen
Wenn Sie einen Körper für die Physik-Engine beschreiben, reichen in vielen Fällen einfache Formen aus. Beispielsweise kann als Annäherung für eine Ellipse ein Acht- oder Zehneck vollkommen genügen, und ein höherer Detaillierungsgrad (z. B. 36-Eck) bringt keine nennenswerte
Verbesserung. Eine höhere Detailgenauigkeit kann sich stattdessen sogar negativ in einer aufwändigeren und langsameren Berechnung der Physik-Engine niederschlagen. Bei animierten
Objekten, z. B. laufenden Spielfiguren, ist eine genaue Annäherung des physikalischen Körpers
häufig auch nicht möglich.
In Listing 6.123 sehen Sie die Erweiterungen der Methode rectangleWithColor:size:posi-
Listing 6.121 Ausschalten der Schwerkraft
tion: aus Listing 6.112.
6 http://de.wikipedia.org/wiki/Erdbeschleunigung
7 http://de.wikipedia.org/wiki/Satz_des_Pythagoras
726
Einführung in das Sprite-Kit
727
6
Models, Layer, Animationen
- (SKNode *)rectangleWithColor:(SKColor *)inColor size:(CGSize)inSize position:
(CGPoint)inCenter {
SKShapeNode *theRectangle = [SKShapeNode new];
CGRect theBounds = CGRectMake(-inSize.width / 2.0, -inSize.height / 2.0,
inSize.width, inSize.height);
CGPathRef thePath = CGPathCreateWithRect(theBounds, NULL);
SKPhysicsBody *theBody = [SKPhysicsBody bodyWithRectangleOfSize:inSize];
...
theRectangle.userData = [NSMutableDictionary new];
theBody.dynamic = NO;
theRectangle.physicsBody = theBody;
CGPathRelease(thePath);
return theRectangle;
}
6.5
Einführung in das Sprite-Kit
Um den Ball in Bewegung zu versetzen, weist die Methode ihm auch noch eine Geschwindigkeit in Form eines Vektors an die Property velocity zu. Dabei soll jedes neue Spiel eine
andere Richtung verwenden, um möglichst gleiche Spielabläufe zu vermeiden. Damit der
Ball jedoch immer zunächst in Richtung der Steine fliegt, muss der Winkel des Geschwindigkeitsvektors in dem Intervall [π / 4, 3π / 4] liegen (siehe Abbildung 6.46).
3π/4
π
π/4
0
Listing 6.123 Physikalischen Körper für Rechtecke anlegen
Abbildung 6.46 Startwinkel des Balls
Bei der physikalischen Beschreibung des Balls dient ein Kreis als Grundlage, der den gleichen
Radius wie der dazugehörige Knoten hat. Damit der Ball nicht nach unten fällt, sollte ihn die
Schwerkraft nicht beeinflussen. Zwar könnten Sie die Schwerkraft auch durch entsprechend
entgegen gerichtete Kräfte annullieren, es geht jedoch wesentlich einfacher, indem Sie die
Property affectedByGravity auf NO setzen. Außerdem weist die Methode dem Körper eine
Masse von 10 zu; ob das nun Gramm, Pfund, Karat oder Unzen sind, sei dahingestellt.
- (SKNode *)ballWithRadius:(CGFloat)inRadius position:(CGPoint)inCenter {
SKShapeNode *theBall = [SKShapeNode new];
CGRect theBounds = CGRectMake(-inRadius, -inRadius, 2 * inRadius, 2 * inRadius);
CGPathRef thePath = CGPathCreateWithEllipseInRect(theBounds, NULL);
SKPhysicsBody *theBody = [SKPhysicsBody bodyWithCircleOfRadius:inRadius];
CGFloat theAngle = M_PI / 4.0 + M_PI * drand48() / 2.0;
...
theBall.position = inCenter;
theBody.affectedByGravity = NO;
theBody.mass = 10;
theBody.velocity = CGVectorMake(self.velocity * cosf(theAngle),
self.velocity * sinf(theAngle));
theBall.physicsBody = theBody;
CGPathRelease(thePath);
return theBall;
}
Listing 6.124 Beschreibung des physikalischen Körpers des Balls
728
Die Methode ermittelt dazu über die Funktion drand48() eine Fließkomma-Pseudozufallszahl
zwischen 0 und 1, und somit liegt der Wert der Variablen theAngle zwischen π / 4 und 3π / 4.
Physikalische Beschreibung der Szene
Somit haben Steine, Schläger und Ball eine physikalische Beschreibung; es fehlt nun noch eine
Beschreibung für die Szene, da die Physik-Engine auch diese bei ihren Berechnungen mit einbeziehen soll. Zwar besteht die physikalische Form der Szene wie der Schläger und die Steine
ebenfalls aus einem Rechteck, dennoch dürfen Sie den Körper nicht über bodyWithRectangleOfSize: erzeugen. Stattdessen verwenden Sie den Convenience-Konstruktor bodyWithEdgeLoopFromRect:. Das erzeugte Objekt beschreibt eine masselose, unbewegliche, rechteckige
Umrandung, bei der andere Objekte mit der Innenseite kollidieren. Im Gegensatz dazu kollidiert der Ball mit der Außenseite der Steine und des Schlägers.
CGSize theBricklSize = self.brickSize;
CGSize theRacketSize = CGSizeMake(theBricklSize.width, theBricklSize.height / 2.0);
CGRect theFrame = self.frame;
SKPhysicsBody *theBody;
self.backgroundColor = [SKColor blueColor];
self.velocity = inSize.height / 2.0;
[self buildBricks];
self.racket = [self racketWithSize:theRacketSize position:CGPointMake(inSize.width /
2.0, theBricklSize.height)];
[self addChild:self.racket];
729
6
Models, Layer, Animationen
theFrame.origin.y = -2.0 * theBricklSize.height;
theFrame.size.height += 2.0 * theBricklSize.height;
theBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:theFrame];
self.physicsBody = theBody;
[self start];
Listing 6.125 Physikalische Beschreibung der Szene
Wie Sie in Listing 6.125 sehen können, ragt die physikalische Umrandung der Szene über den
unteren Rand um die doppelte Steinhöhe hinaus, da der Ball ja schließlich bei der Überschreitung des unteren Randes verschwinden soll. Da die Abschaltung einzelner Randstücke
leider nicht möglich ist, müssen wir hier zu diesem kleinen Trick greifen.
Damit haben Sie die physikalische Beschreibung der Spielobjekte weitestgehend abgeschlossen. Wenn Sie das Spiel starten, prallt der Ball zwar am ersten Stein ab, seine Geschwindigkeit
nimmt jedoch zusehends ab, bis er schließlich stehen bleibt. Außerdem verbleibt der vom
Ball getroffene Stein im Spiel. Sie können das am Beispielprojekt übrigens nachvollziehen,
indem Sie das Makro PHYSICS am Anfang der Datei BreakoutScene.m auf 1 setzen.
6.5.5 Auf Kollisionskurs
Zwar haben Sie eine physikalische Beschreibung für die Objekte angelegt, es fehlt allerdings
noch eine Kollisionsbehandlung. Da das Sprite-Kit den Ball automatisch von den anderen
Objekten abprallen lässt, braucht sich das Spiel bei der Kollision des Balls mit einem Objekt nur
um die zusätzlichen Operationen zu kümmern. Dazu gehören beispielsweise das Entfernen
der Steine, das Abspielen von Tönen oder das Spielende bei Berührung des unteren Randes.
Über die Property contactDelegate des Physics-World-Objekts können Sie ein Delegate festlegen, mit dem Sie sich über Kollisionen benachrichtigen lassen können. Das zugehörige
Protokoll SKPhysicsContactDelegate deklariert die Methoden didBeginContact: und didEndContact:, die jeweils ein Objekt der Klasse SKPhysicsContact als Parameter erhalten.
Diese Klasse besitzt vier nur lesbare Propertys, die die Kollision beschreiben. Die Propertys
bodyA und bodyB verweisen auf die kollidierten, physikalischen Körper. Den Kontaktpunkt
können Sie über contactPoint auslesen, und collisionImpulse beschreibt die Stärke des
Zusammenpralls.
Die Kollisionserkennung des Sprite-Kits erlaubt eine Unterteilung der Objekte in Kategorien,
die Sie über die Property categoryBitMask dem jeweiligen physikalischen Körper zuweisen.
Wie der Name schon verrät, handelt es sich dabei um eine Bitmaske, und jedes Bit in dem
Wert repräsentiert eine Kategorie. Das Breakout-Spiel verwendet vier Kategorien, und Sie
finden in Listing 6.126 die Definitionen für entsprechende Konstanten:
const uint32_t kWallMask = 1 << 0;
const uint32_t kBallMask = 1 << 1;
730
6.5
Einführung in das Sprite-Kit
const uint32_t kBrickMask = 1 << 2;
const uint32_t kRacketMask = 1 << 3;
Listing 6.126 Konstanten-Definitionen für die Kategorien der Körper
Diese Bitmasken können Sie für die Auswertung der Kollisionen nutzen, um festzustellen,
welche Art das Objekt hat, mit dem der Ball zusammengestoßen ist. Dabei ist die Zuordnung
der Objekte zu den Propertys jedoch leider nicht fest. Wenn beispielsweise der Ball mit
einem Stein kollidiert, kann die Property bodyA sowohl auf den Körper des Balls als auch auf
den des Steins verweisen, und entsprechend bodyB auf den Körper des Steins beziehungsweise den des Balls.
Bei dem Breakout-Spiel gibt es vier mögliche Körperkategorien, und damit theoretisch 16
unterschiedliche Fälle, die bei einer Kollision auftreten können. Da der Ball das einzige
bewegliche Objekt im Spiel ist, muss er bei jeder Kollision beteiligt sein, und deswegen
beschränkt sich die Anzahl der möglichen Fälle auf sechs.
Allerdings lässt sich durch Hilfsmethoden die Anzahl noch weiter verringern. Die Hilfsmethoden bekommen einen physikalischen Körper und gegebenenfalls den Kollisionspunkt
übergeben, und testen für den Körper alle möglichen Kollisionspartner des Balls durch.
Dadurch verringern Sie die notwendigen Abfragen auf drei, da Sie die Hilfsmethode für beide
Kollisionskörper jeweils einmal aufrufen können.
- (void)didBeginContact:(SKPhysicsContact *)inContact {
CGPoint thePoint = inContact.contactPoint;
[self didBeginContactWithBody:inContact.bodyA atPoint:thePoint];
[self didBeginContactWithBody:inContact.bodyB atPoint:thePoint];
}
Listing 6.127 Delegate-Methode für den Beginn einer Kollision
Die Hilfsmethode didBeginContactWithBody:atPoint: ruft die Delegate-Methode jeweils einmal für beide Körper einer Kollision auf. Bei einer Kollision mit einem Stein entfernt sie diesen über eine Aktionssequenz aus dem Spielfeld, erhöht den Spielstand und simuliert eine
Explosion über die Methode explosionAtPoint:. Im Fall eines Zusammenstoßes des Balls mit
einer Wand gibt es zwei Möglichkeiten. Eine Berührung des unteren Spielfeldrands beendet
das Spiel, während die App bei einem Zusammenprall mit den anderen Wänden einen Ton
abspielt, was sie auch beim Zusammentreffen mit dem Schläger macht.
- (void)didBeginContactWithBody:(SKPhysicsBody *)inBody atPoint:(CGPoint)inPoint {
if(inBody.categoryBitMask == kBrickMask) {
SKNode *theBrick = inBody.node;
SKAction *theAction = [SKAction sequence:@[[SKAction fadeOutWithDuration:
0.5], [SKAction removeFromParent]]];
731
6
Models, Layer, Animationen
[theBrick runAction:theAction];
self.score += [theBrick.userData[kPoints] unsignedIntegerValue];
[self explosionAtPoint:inPoint];
}
else if(inBody.categoryBitMask == kWallMask) {
if (inPoint.y < 0.0) {
[self stop];
}
else {
[self runAction:self.contactWall];
}
}
else if(inBody.categoryBitMask == kRacketMask) {
[self runAction:self.contactRacket];
}
}
Listing 6.128 Ballkollisionen auswerten
Das Sprite-Kit erlaubt auch das Abspielen von Tönen über Aktionen. Die entsprechenden
Aktionen hält sie dabei in den Propertys contactRacket, contactWall und explosion, die sie in
der Methode initWithSize: über den Convenience-Konstruktor playSoundFileNamed:waitForCompletion: initialisiert. Sie können diese Aktionen beliebig häufig verwenden, um diese
Töne abzuspielen.
self.contactRacket = [SKAction playSoundFileNamed:@"racket.caf" waitForCompletion:
NO];
self.contactWall = [SKAction playSoundFileNamed:@"wall.caf" waitForCompletion:NO];
self.explosion = [SKAction playSoundFileNamed:@"explosion.caf" waitForCompletion:NO];
Listing 6.129 Initialisierung der Töne für das Spiel
Wenn der Ball von einer Wand oder dem Schläger abprallt, erhält er noch einen zusätzlichen
Impuls, um ihn zu beschleunigen und so ein Ausrollen zu verhindern. Analog zur Verarbeitung des Kontaktbeginns erfolgt die auch Verarbeitung des Kontaktendes über eine Delegate- sowie eine Hilfsmethode.
- (void)didEndContact:(SKPhysicsContact *)inContact {
[self didEndContactWithBody:inContact.bodyA];
[self didEndContactWithBody:inContact.bodyB];
}
732
6.5
Einführung in das Sprite-Kit
- (void)didEndContactWithBody:(SKPhysicsBody *)inBody {
if(inBody.categoryBitMask == kBrickMask) {
SKNode *theBrick = inBody.node;
CGFloat theImpulse = [theBrick.userData[kImpulse] floatValue];
[self addImpulseWithFactor:theImpulse];
}
else if(inBody.categoryBitMask & (kWallMask | kRacketMask)) {
[self addImpulseWithFactor:15.0];
}
}
Listing 6.130 Kontaktende verarbeiten
Während der Impulsfaktor beim Zusammenstoß mit dem Schläger immer den Wert 15
hat, ist er bei den Steinen unterschiedlich stark. Dazu hat die Methode buildBricks aus Listing 6.112 einen Wert in den Nutzerdaten der Knoten für die Steine abgelegt. Die Methode
addImpulseWithFactor: erzeugt aus dem Faktor und der Bewegungsrichtung des Balls einen
Impulsvektor und wendet ihn auf den Ball an.
- (void)addImpulseWithFactor:(CGFloat)inFactor {
SKPhysicsBody *theBody = self.ball.physicsBody;
CGVector theVelocity = theBody.velocity;
CGFloat theLength = CGVectorLength(theVelocity);
CGFloat theImpulse = theBody.mass * self.gravity * inFactor;
CGVector theImpulseVector = CGVectorMake(theImpulse * theVelocity.dx / theLength,
theImpulse * theVelocity.dy / theLength);
[theBody applyImpulse:theImpulseVector];
}
Listing 6.131 Beschleunigung des Balls durch einen Impuls
Der Impuls soll dabei in die gleiche Richtung wie die Ballbewegung erfolgen, aber von der
aktuellen Geschwindigkeit des Balls unabhängig sein. Wenn Sie den Geschwindigkeitsvektor durch seine Länge teilen, erhalten Sie einen Vektor mit der gleichen Richtung und
der Länge eins. Diesen Vektor können Sie nun mit dem Impuls multiplizieren, um den
gesuchten Impulsvektor zu erhalten. Diese Berechnung entspricht dem Wert, den der Code
aus Listing 6.131 der Variablen theImpulseVector zuweist.
Das Spiel zeigt Zusammenstöße des Balls nun durch akustische Signale an. Wenn der Ball mit
einem Stein zusammentrifft, entfernt das Spiel den Stein. Dies soll durch eine Explosion
visualisiert werden.
733
6
Models, Layer, Animationen
6.5
6.5.6 Freude schöner Götterfunken
Vorlage
Beschreibung
des Spielgeschehens. Dazu stellt das Sprite-Kit Partikel-Emitter über die Klasse SKEmitterNode
Bokeh
Bouquet eines Höhenfeuerwerks
bereit, mit denen Sie die Aussendung von Teilchen aus einer Quelle simulieren können.
Fire
Feuer
Dabei befolgen die Teilchen die Gesetze der physikalischen Welt des Spiels, und so beeinflus-
Fireflies
Transparente, wachsende, gelbe Lichtpunkte mit Ausblendeffekt
die Aussendung der Teilchen mit über 40 Parametern steuern, und so lassen sich eine Viel-
Magic
Das Selbe in grün
zahl von Effekten mit Partikel-Emittern simulieren.
Rain
Schräg fallender Dauerregen
Die Konfiguration von Partikel-Emittern gestaltet sich aufgrund der vielen Parameter recht
Smoke
Schwarzer, aufsteigender Rauch
Snow
Fallende Schneeflocken
Spark
Funkensprühen
Viele Spiele nutzen Explosionen, sprühende Funken oder Dämpfe zur optischen Aufwertung
sen beispielsweise die Gravitation und die Reibung die Flugbahn der Teilchen. Sie können
Einführung in das Sprite-Kit
aufwendig, und glücklicherweise stellt Xcode dafür einen speziellen Editor zur Verfügung,
der diese Aufgabe erheblich vereinfacht (siehe Abbildung 6.47).
Tabelle 6.10 Vorlagen für Partikel-Emitter
Xcode legt für einen Partikel-Emitter in der Regel zwei Dateien an: Eine Datei mit der Endung
.sks, die die Beschreibung des Emitters enthält, sowie ein Bild für die Darstellung der Partikel.
Sie können diese Beschreibung laden, um einen neuen Emitter-Knoten zu erzeugen. Dazu
bestimmen Sie den Pfad auf die .sks-Datei und deserialisieren das enthaltene Objekt über die
Klassenmethode unarchiveObjectWithFile: der Klasse NSKeyedUnarchiver.
- (SKEmitterNode *)emitterNodeNamed:(NSString *)inName {
NSString *thePath = [[NSBundle mainBundle] pathForResource:inName ofType:@"sks"];
return [NSKeyedUnarchiver unarchiveObjectWithFile:thePath];
}
Listing 6.132 Emitter-Knoten aus einer Datei laden
Abbildung 6.47 Konfiguration eines Partikel-Emitter-Knotens
Wenn Sie über File 폷 New 폷 File... die Dateivorlage iOS 폷 Resources 폷 SpriteKit Particle
File auswählen, können Sie einen neuen Partikel-Emitter anlegen. Im nächsten Schritt bietet Xcode eine Reihe mit vorkonfigurierten Effekten an, die Tabelle 6.10 auflistet und
beschreibt.
Eine Explosion soll im Breakout-Spiel den Zusammenstoß des Balls mit einem Stein optisch
aufpeppen. Der Partikel-Emitter dafür basiert auf der Bokeh-Vorlage und den Einstellungen
aus Abbildung 6.47, wobei wir diese Werte durch Ausprobieren gefunden haben. Dabei
haben wir uns bei einigen Einstellungen natürlich auch von offensichtlichen Eigenschaften
einer realen Explosion leiten lassen:
왘 Die Werte für Particles und Lifetime legen die Anzahl und die Lebensdauer der Partikel
fest. Bei einer Explosion entstehen sehr viele Partikel in einem sehr kurzen Zeitraum. Die
Gesamtanzahl (1.000) der erzeugten Partikel ist niedriger als die Anzahl der Partikel, die
der Emitter pro Zeitintervall erzeugt, wodurch sich der Emissionszeitraum verkürzt. Dieser kurze Zeitraum verstärkt den Explosionseffekt erheblich.
왘 Die Teilchen einer Explosion treten aus einer punktförmigen Quelle aus, was sich über
den Wert 0 für X und Y unter Position Range erreichen lässt.
734
735
6
Models, Layer, Animationen
왘 Bei einer Explosion streuen die Partikel in alle Richtungen gleichermaßen, deswegen
kann der Austrittswinkel (Angle) Werte aus dem Bereich von 0° bis 360° annehmen.
왘 Die Partikel einer Explosion haben direkt zu Beginn eine sehr hohe, fast gleiche Geschwin-
digkeit (Speed). Hierfür hat sich der Wert 500 und ein Bereich von 0 bewährt.
왘 Durch den Wert und Bereich für Scale erzeugt der Emitter sehr kleine Teilchen wie bei
einer Sprengstoffexplosion. Mit größeren Werten (z. B. 10) lässt sich eher eine thermonukleare Explosion simulieren.
왘 Die Farben der Partikel bewegen sich in einem gelb-orangefarbigen Spektrum (Color
6.6
Über diese Brücke musst du gehen
der Klassen auf Datentypen und umgekehrt abbildet. Die Toll-free Bridge funktioniert allerdings nur mit Datentypen und den zugehörigen Klassen von Apple; Sie können diesen
Mechanismus leider nicht auf andere Klassen und Datentypen ausdehnen.8
Beim manuellen Referenzzählen besteht die Brücke einfach aus einem Cast. Sie können also
beispielsweise ein Objekt der Klasse NSString auf eine Referenz des Typs CFStringRef und
umgekehrt abbilden:
CFStringRef theReference = (CFStringRef)theString;
NSString *theValue = (NSString *)theReference;
Ramp).
Listing 6.134 Toll-free Bridge
Bis auf die Textur (Particle Texture) spielen die anderen Einstellungswerte für den Effekt
keine entscheidende Rolle. Xcode hat für die Textur bereits ein Bild mit dem Dateinamen
spark.png zum Projekt hinzugefügt. Sie können dieses Bild natürlich auch gegen ein anderes
austauschen, wodurch sich interessante neue Effekte ergeben können, die in der Regel
jedoch den Explosionseffekt stark verfremden.
6.6.1 Toll-free Bridging und ARC
Beim automatischen Referenzzählen müssen Sie dem Compiler hingegen einen zusätzlichen
Hinweis für die Speicherverwaltung geben. In Core Foundation erfolgt die Speicherverwaltung
ja nach wie vor explizit durch Aufrufe der Funktionen CFRetain und CFRelease, und deshalb
müssen Sie dem Compiler mitteilen, wie Sie das Core-Foundation-Objekt verwalten möchten.
Für die Darstellung einer Explosion fügen Sie den Emitter-Knoten am Kontaktpunkt von Ball
und Stein zu der Szene hinzu, und entfernen ihn nach einer halben Sekunde wieder. Dabei
können Sie über die Klassenmethode waitForDuration: eine Warte-Aktion erzeugen, um die
Entfernung des Emitter-Knotens entsprechend zu verzögern. Den Code dazu sehen Sie in
Listing 6.133.
Im einfachsten Fall wollen Sie die Eigentümerschaft nicht übertragen, und die Speicherverwaltung bleibt beim Ursprungsobjekt. Dafür nehmen Sie das Schlüsselwort __bridge in den
Cast mit auf.
- (void)explosionAtPoint:(CGPoint)inPoint {
SKNode *theNode = [self emitterNodeNamed:@"explosion"];
SKAction *theAction = [SKAction sequence:@[[SKAction waitForDuration:0.5],
[SKAction removeFromParent]]];
CFStringRef *theReference = CFStringCreateCopy(...);
NSString *theString = (__bridge NSString *)theReference;
...
CFRelease(theReference);
theNode.position = inPoint;
[self addChild:theNode];
[theNode runAction:theAction];
[theNode runAction:self.explosion];
}
Listing 6.133 Eine Explosion in der Szene auslösen
6.6 Über diese Brücke musst du gehen
Core Foundation ist ein C-Framework, das die Grundlage für das Foundation-Framework bildet. Es stellt allerdings Funktionen und keine Objective-C-Klassen bereit, und so müssen Sie
die Parameter und Rückgabewerte gegebenenfalls von den Klassen auf die entsprechenden
Datentypen übertragen. Das ist jedoch glücklicherweise in den meisten Fällen nicht sehr
schwer, da Apple hierfür einen speziellen Mechanismus – die Toll-free Bridge – bereitstellt,
736
Listing 6.135 Cast ohne Übertragung der Eigentümerschaft
Listing 6.135 überträgt die Core-Foundation-Referenz theReference ohne die Eigentümerschaft auf eine Referenz von NSString. Dadurch erzeugt der ARC-Compiler keinen Code, um
die Zeichenkette, auf die theString verweist, freizugeben. Damit kein Speicherleck entsteht,
müssen Sie also die Core-Foundation-Referenz durch CFRelease freigeben. Auch bei dem
umgekehrten Cast in Listing 6.136 überträgt der ARC-Compiler nicht die Eigentümerschaft.
Dadurch sorgt er für die Freigabe der Zeichenkette, auf die theString verweist. Ein Aufruf von
CFRelease für theReference würde bei diesem Beispiel wahrscheinlich zu einem Speicherzugriffsfehler führen.
NSString *theString = ...;
CFStringRef theReference = (__bridge CFStringRef)theString;
Listing 6.136 Umgekehrter Cast ohne Übertragung der Eigentümerschaft
8 Zumindest nicht, wenn Sie Ihre App durch den Review-Prozess in den App Store bekommen möchten.
737
6
Models, Layer, Animationen
Listing 6.60 enthält einen solchen Cast: (__bridge CFURLRef)theURL. Hier soll die Verwaltung
der Eigentümerschaft von theURL beim ARC-Compiler bleiben, da ja die Funktion CGImageSourceCreateWithURL sie nicht übernimmt. Die Funktion CGImageSourceCreateImageAtIndex liefert hingegen ein Core-Graphics-Bild zurück, dessen Eigentümer die Methode ist.
Sie muss nun das Bild in das Array einfügen und dabei die Eigentümerschaft abgeben. Das
erreichen Sie über das Schlüsselwort __bridge_transfer. Der Cast (__bridge_transfer
id)theImage wandelt das Bild in ein Objekt um und teilt dem ARC-Compiler mit, dass er die
Verwaltung der Eigentümerschaft übernehmen soll.
Der Modifizierer __bridge_retained dient für den entgegengesetzten Fall, wenn Sie ein
Objekt in ein Core-Foundation-Objekt umwandeln und der ARC-Compiler die Verwaltung
der Eigentümerschaft dabei abgeben soll. Dieser Fall tritt beispielsweise bei Erzeugungs- und
Kopierfunktionen auf:
CFStringRef CreateStringWithInteger(NSInteger inValue) {
NSString *theValue = [[NSString alloc] initWithFormat:@"%d", inValue];
return (__bridge_retained CFStringRef)theValue;
}
Listing 6.137 Übergabe der Eigentümerschaft an Core Foundation
Durch das __bridge_retained erzeugt der ARC-Compiler nicht den Code für die Freigabe der
Zeichenkette, auf die theValue verweist, und somit hält der Aufrufer der Funktion CreateStringWithInteger die Zeichenkette.
Sie können auch anders
Sie können statt __bridge_retained auch die Funktion CFBridgingRetain verwenden und
den Cast in Listing 6.137 als (CFStringRef)CFBridgingRetain(theValue) schreiben. Auch für
__bridge_transfer gibt es mit der Funktion CFBridgingRelease eine Alternative. Sie können
den Cast beim Einfügen in das Array in Listing 6.60 also auch als CFBridgingRelease(theImage) schreiben.
Der Analyzer erkennt übrigens auch Speicherverwaltungsfehler, die durch falsche Modifizierer
beim Toll-free Bridging entstehen. Sie können ihn über Product 폷 Analyze starten, und er
zeigt Ihnen mögliche Schwachstellen Ihres Codes an.
6.6.2 C-Frameworks und ARC
Das Toll-free Bridging funktioniert sogar, wenn es für einen Typ keine entsprechende Klasse
gibt oder umgekehrt. Dieser Fall tritt auch schon in Listing 6.60 auf; der Cast (__bridge_
transfer id)theImage funktioniert, obwohl es keine Klasse (inklusive UIImage) in Cocoa
Touch gibt, die dem Datentyp CGImage entspricht.
738
6.7
Was Sie schon immer über Instruments wissen wollten, aber nie zu fragen wagten
Dass der Cast dennoch funktioniert, liegt daran, dass auch viele Datentypen in den C-Frameworks sich in einer Typhierarchie mit einer gemeinsamen Menge von Funktionen befinden.
Diese gemeinsamen Funktionen bilden dabei genau die Methoden ab, die eine Objective-CKlasse implementieren muss, die das Protokoll NSObject implementiert.
Das bedeutet, dass Sie sowohl alle Referenzen aus Core Foundation als auch aus den anderen
C-Frameworks (wie Core Graphics, ImageIO oder Address Book) zumindest wie Referenzen
auf NSObject behandeln können. Beim manuellen Referenzzählen entspricht also beispielsweise die Anweisung CFRetain(theImage) der Anweisung [(id)theImage retain]. Diese transparente Abbildung der C-Datentypen auf Objective-C-Klassen hat nun auch den schönen
Nebeneffekt, dass Sie, wie in Listing 6.60 gezeigt, Core-Graphics-Objekte in einem Foundation-Array sammeln können.
6.7 Was Sie schon immer über Instruments wissen wollten,
aber nie zu fragen wagten
Und nun zu etwas ganz anderem: Wie gut kennen Sie eigentlich noch die Speicherverwaltungsregeln? In welchen Fällen hält noch mal eine Variable das Objekt, auf das sie verweist?
Und wie war das noch mal bei Core Foundation? Wenn Sie sich jetzt unsicher sind, dürfen Sie
natürlich gerne zurückblättern. Allerdings wird Ihnen auch dann, wenn Sie die Regeln und
ihre Anwendung beherrschen, wahrscheinlich von Zeit zu Zeit der eine oder andere Speicherverwaltungsfehler unterlaufen. Wenn Sie Glück haben, sendet Ihnen Ihr Programm
zarte Hinweise. Im schlechtesten Fall macht es das indes nur bei den Nutzern Ihrer App. Nach
Murphys Gesetz – »Was schiefgehen kann, geht schief« – ist Letzteres der Regelfall.
Sie können und sollten den Analyzer über Ihren Programmcode laufen lassen und versuchen, möglichst alle Warnungen zu beseitigen. Leider findet auch der Analyzer nicht alle Fehler, und auch mit dem Debugger mogelt sich schon mal die eine oder andere Schwachstelle
durch. Es gibt jedoch noch eine weitere Möglichkeit, Schwachstellen im Programm zu finden: Instruments und Arbeit. Da Sie von Arbeit sicherlich schon mal gehört haben, dreht sich
dieser Abschnitt hauptsächlich um Instruments.
Instruments ist ein Programm aus den Entwicklungswerkzeugen von Apple, das Sie bereits
zusammen mit dem SDK installiert haben. Es erlaubt die Aufzeichnung und anschließende
Analyse unterschiedlicher Messwerte Ihrer Programme, um deren Schwachstellen zu finden.
Es kann Ihnen jedoch nur Hinweise geben, wo Sie nach den Schwachstellen suchen müssen.
Es macht Ihnen leider keine Vorschläge zu deren Behebung.
Instruments funktioniert dabei nach einem Baukastensystem, bei dem jeder Baustein ein
Messinstrument ist. Sie können für Ihre Instruments-Sitzungen diese Bausteine beliebig
kombinieren. Zurzeit bietet Instruments ungefähr fünfzig solcher Messinstrumente an, von
denen allerdings einige OS X vorbehalten sind.
739
6
Models, Layer, Animationen
Sie starten Instruments, indem Sie den Button Run oben links in Xcode gedrückt halten und
im Dropdown-Menü den Punkt Profile auswählen (siehe Abbildung 6.48).
6.7
Was Sie schon immer über Instruments wissen wollten, aber nie zu fragen wagten
Operation am lebenden Programm
Damit Sie einen anschaulichen Eindruck von Instruments bekommen, finden Sie im Git-Repository zum Buch das Beispielprojekt Instruments im Unterverzeichnis https://github.com/
Cocoaneheads/iPhone/tree/Ausgabe_iOS8/Apps/iOS6/Instruments.
Abbildung 6.48 Starten von Instruments
Nach dem Start öffnet Instruments den in Abbildung 6.49 dargestellten Auswahldialog, mit
dem Sie ein bestehendes Instruments-Dokument oder eine Dokumentvorlage auswählen
können. In der Regel benutzen Sie die zweite Option. Hier bietet Ihnen Instruments bereits
eine Palette mit vorkonfigurierten Messszenarien an. Alternativ können Sie auch eine leere
Vorlage auswählen, die Sie individuell nach Ihren Bedürfnissen konfigurieren können. Für
den Anfang sollten Sie lieber ein vorkonfiguriertes Szenario verwenden. Falls Instruments
Ihr Programm sofort startet, ohne vorher den Auswahldialog anzuzeigen, haben Sie wahrscheinlich das Programm noch in Instruments geöffnet. Schließen Sie also am besten immer
alle Fenster von Instruments, bevor Sie eine neue Analyse starten.
Die App stellt verschiedene Analysesituationen bereit und enthält deshalb absichtlich
Schwachstellen für die nachfolgend beschriebenen Messszenarien, und alle nachfolgenden
Beispiele beziehen sich auf diese App. Die Schwachstellen im Beispielprojekt sind relativ offensichtlich. Wahrscheinlich verstecken sie sich in Ihren Programmen wesentlich besser. Hier
kommt dann das zweite Gegenmittel ins Spiel: Arbeit.
6.7.1 Spiel mir das Lied vom Leak
Eine der wichtigsten Aufgaben von Instruments ist das Auffinden von Speicherlecks. Speicherlecks führen im schlimmsten Fall nicht direkt zu einem Fehlverhalten Ihres Programms.
Stattdessen erhält es vom Betriebssystem Speicherwarnungen, deren Verarbeitung die App
verlangsamen. Schließlich stürzt das Programm dann doch ab, wobei sich die Absturzsituation nur sehr schwer reproduzieren lässt.
Das automatische Referenzzählen verhindert zwar in vielen Fällen Speicherlecks; es gibt
dennoch viele Situationen, in denen Speicherlecks auftreten können:
1. bei Einsatz der C-Frameworks von Apple (z. B. Core Foundation, Core Graphics)
2. bei Verwendung von malloc, calloc, new (C++) oder anderer Speicherverwaltungsfunktionen und -operatoren
3. beim Einsatz von Bibliotheken mit manuellem Referenzzählen
4. Retain-Zyklen; also Objekte, die sich gegenseitig im Speicher halten, wie beispielsweise
Erna und Fritz aus Kapitel 2, »Die Reise nach iOS«
Die Vorlage Leaks mit dem gleichnamigen Messinstrument kann Ihnen helfen, auch solche
Speicherverwaltungsfehler zu finden.
Abbildung 6.49 Auswahldialog in Instruments
740
Öffnen Sie das Instruments-Projekt in Xcode, und starten Sie Instruments wie oben
beschrieben. Im Auswahldialog (siehe Abbildung 6.49) wählen Sie die Schablone Leaks aus,
mit der Sie Speicherlecks finden können. Instruments startet Ihre App, abhängig von Ihrer
Auswahl in Xcode, im Simulator oder auf einem iOS-Gerät. Außerdem öffnet es das in Abbildung 6.50 dargestellte Fenster. Die Schablone erzeugt zwei Messinstrumente, die Sie in der
linken Spalte oben finden. Mit Allocations messen Sie den Speicherverbrauch Ihrer App,
während Leaks die Speicherlecks aufzeichnet. Wenn Sie im oberen Bereich des Fensters ein
Messinstrument auswählen, zeigt Instruments Ihnen im unteren Bereich dazu Detailinformationen an.
741
6
Models, Layer, Animationen
6.7
Was Sie schon immer über Instruments wissen wollten, aber nie zu fragen wagten
Oben links im Fenster befindet sich der Stop-Button – ein schwarzes Quadrat – an der Stelle,
wo Instruments vorher den Aufnahme-Button angezeigt hat. Damit können Sie die Ausführung der App beenden, wenn Sie genug Leaks gesammelt haben. Durch Auswählen des
Leaks-Instruments in der Spalte Instruments (siehe Abbildung 6.51) zeigt das Programm
im unteren Fensterbereich Details zu den gefundenen Leaks an. Die linke Seite listet die
gefundenen Speicherlecks auf, während Sie in der rechten Spalte die Einstellungen des
Instruments anpassen können (siehe Abbildung 6.52).
Abbildung 6.50 Speicheranalyse mit Instruments
Nach dem Start der Aufzeichnung über den roten Aufnahme-Button oben rechts erscheint
neben Allocations ein blaues »Gebirge«, das den verbrauchten Speicher der App symbolisiert. Dabei stellen die Höhen keine absoluten, sondern relative Werte zum maximalen Speicherverbrauch dar, und Sie erkennen große Speicherreservierungen daran, dass weiter links
liegende Teile des Gebirges plötzlich stark abflachen.
Der Bereich neben Leaks bleibt zunächst leer, da das Programm bislang noch kein Speicherleck erzeugt hat. Wenn Sie jedoch den Knopf malloc in der iPhone-App drücken, erscheint
im Leaks-Bereich nach einer Verzögerung ein roter Balken (siehe Abbildung 6.51). Der Balken kennzeichnet das Auftreten eines Speicherlecks.
Abbildung 6.52 Details des Leaks-Instruments
Einstellungssache
Leaks sammelt die Speicherlecks in regelmäßigen Intervallen. Die Intervalllänge ist standardmäßig auf zehn Sekunden eingestellt, weswegen Instruments den roten Balken für das Speicherleck auch erst mit einer Verzögerung anzeigt. Über das Eingabefeld Snapshot Interval
können Sie diesen Wert anpassen. Das geht auch, während eine Messung läuft. Sie können
diese Wartezeit allerdings auch durch Drücken des Buttons Snapshot Now abkürzen. Dann
sammelt Leaks augenblicklich alle Speicherlecks.
Wenn Sie den Pfeil eines Speicherlecks in der Spalte Address (siehe den Mauszeiger in
Abbildung 6.52) anklicken, gelangen Sie zu den Details des Lecks. Hier sehen Sie unter anderem den Referenzzähler und die Erzeugungszeit relativ zum Programmstart. Durch DrüAbbildung 6.51 Grafische Anzeige eines Speicherlecks in Instruments
cken des Reiters Leaks by Backtrace gelangen Sie wieder zurück zur Übersicht mit allen
Speicherlecks.
742
743
6
Models, Layer, Animationen
Sie können sich über (cmd)+(E) oder den Menüpunkt View 폷 Inspectors 폷 Show Extended
Detail den Aufrufstapel anzeigen lassen (siehe Abbildung 6.53), der zu der Programmstelle
führt, an der das Programm den Speicher des Lecks reserviert hat. Sie bekommen darüber in
vielen Fällen einen recht guten Hinweis, wo Sie mit der Fehlersuche für die Speicherverwaltung beginnen sollten.
6.7
Was Sie schon immer über Instruments wissen wollten, aber nie zu fragen wagten
Sie befindet sich in der Methode makeMallocLeak. Die Anweisung reserviert Speicher und
weist ihn einer lokalen Variablen zu. Da die Methode diesen Speicher allerdings nicht wieder
freigibt und ihn auch nicht weitergibt, so dass ihn eine andere Methode freigeben könnte,
entsteht hier ein Speicherleck. Dieser Speicherverwaltungsfehler ist also ziemlich offensichtlich, und übrigens entdeckt auch der Analyzer diesen Fehler.
Zurück auf Los
Sie können von der Detailansicht zu den allgemeineren Ansichten zurückkehren, indem Sie
auf die Pfeilsymbole oberhalb der Detailansicht (siehe die oberste Zeile in Abbildung 6.52)
klicken.
Außerdem können Sie in jeder Ansicht die Spaltenanzeige durch einen Rechtsklick auf die
Titelleiste an Ihre Bedürfnisse anpassen. Dadurch können Sie einzelne Spalten ausblenden. Die
Reihenfolge der Spalten verändern Sie durch Verschieben der Spaltenköpfe und die Zeilensortierung durch einfaches Anklicken der Spaltenköpfe.
Über den Button Attribute in der App erzeugen Sie komplexere Speicherlecks. Damit Instruments ein Speicherleck erkennt, müssen Sie diesen Button allerdings mindestens zweimal
drücken. Wenn Sie sich danach die Details dieses Lecks ansehen, zeigt Instruments Ihnen
mehrere Zeilen statt einer Zeile an (siehe Abbildung 6.55).
Dabei enthält jede Zeile eine Speicherverwaltungsoperation, und der Spalte Event Type können Sie die jeweilige Operation entnehmen. Da der Fehler aus dem Programm und nicht aus
den Systembibliotheken von Apple stammt, brauchen Sie nur die Zeilen zu analysieren, die
Instruments in der Spalte Responsible Library stehen hat. Außerdem können Sie durch
Aktivieren des Buttons Unpaired alle Ereignisse ausblenden, die Instruments als unproblematisch erkannt hat. Dadurch bleibt in der Übersicht eine Zeile aus dem Programm mit dem
Ereignistyp Retain übrig; es ist die selektierte Zeile in Abbildung 6.55.
Abbildung 6.53 Anzeige eines einfachen Lecks mit Aufrufstapel
Durch einen Doppelklick auf eine Zeile im Aufrufstapel können Sie sich die entsprechende
Stelle im Quellcode anzeigen lassen. Das funktioniert natürlich nur für Symbole, deren
Quellcode zum Projekt gehört. In der Regel sind das die Zeilen mit dem schwarzen UserSymbol, wie beispielsweise bei der ausgewählten Zeile in der Abbildung. Wenn Sie einen
Doppelklick auf die Zeile im Stapel ausführen, die in Abbildung 6.53 hervorgehoben ist, zeigt
Instruments Ihnen die folgende Zeile an:
Abbildung 6.55 Detailanzeige für ein komplexeres Leck
In der Spalte Responsible Caller sehen Sie die Methode, die die Operation aufruft. In der
Abbildung 6.54 Hier erzeugt die App ein Speicherleck.
744
ausgewählten Zeile steht dort beispielsweise –[InstrumentsViewController makeAttribute-
745
6
Models, Layer, Animationen
6.7
Was Sie schon immer über Instruments wissen wollten, aber nie zu fragen wagten
Leak]. Dieses Retain hat also die Methode makeAttributeLeak im InstrumentsViewController
ausgelöst. Auch diese Methode scheint sich an die Speicherverwaltungsregeln zu halten, und
beim ersten Aufruf erzeugt sie ja auch noch kein Leck (siehe Listing 6.138).
Instruments und die Systembibliotheken
Zu dieser Zeile kann Instruments Ihnen keinen Quellcode anzeigen, da es keinen Zugang dazu
hat. Der Quellcode für den Autoreleasepool befindet sich schließlich bei Apple und ist nicht
öffentlich. Für die Überprüfung Ihrer Speicherverwaltung ist das auch nicht notwendig. Außerdem sollten Sie die Möglichkeit, dass Cocoa Touch ein Speicherleck erzeugt, erst nach Ausschluss aller anderen Möglichkeiten in Betracht ziehen.9
- (IBAction)makeAttributeLeak {
_attributeLeak = [[InstrumentsDemoObject object] retain];
}
Listing 6.138 Speicherleck bei Attributzuweisung
Abbildung 6.56 Mehrere Verursacher eines Speicherlecks
Das Leck entsteht hier durch zwei Arrays, die sich gegenseitig enthalten, also einen RetainZyklus erzeugen. Obwohl nur zwei Objekte dieses Leck erzeugen, zeigt Instruments drei Speicherbereiche an. Kann Instruments etwa nicht zählen?
Wenn Sie die markierten Stellen in Abbildung 6.56 betrachten, sind die beiden ersten offensichtlich richtig; hier reserviert die Methode Speicher für die beiden Arrays. Die dritte Markierung liegt bei dem Methodenaufruf von addObject:. Da ein veränderliches Objekt jedoch
nahezu beliebig viele Objekte aufnehmen kann, muss es seinen Speicher bei Bedarf vergrößern können, und das ist anscheinend hier geschehen, weshalb Instruments auch diese
Stelle als Speicherleck kennzeichnet.
Wenn Sie die Methode indes wiederholt ausführen, gibt sie jedoch das Objekt, auf das _attributeLeak verweist, nicht frei. Nach den Speicherverwaltungsregeln muss sie das allerdings
machen, da die Variable das Objekt ja hält. Um diesen Speicherverwaltungsfehler zu beheben,
sollten Sie lieber den Setter verwenden.
- (IBAction)makeAttributeLeak {
self.attributeLeak = [InstrumentsDemoObject object];
}
Listing 6.139 Methode ohne Speicherleck
Speicherverwaltung
Der Speicherverwaltungsfehler in der Methode makeAttributeLeak ist für Anfänger schwer zu
erkennen, und sogar der Analyzer in Xcode findet ihn nicht. Sie können solche Fehler jedoch
durch automatisches Referenzenzählen oder die konsequente Verwendung des Accessoren
vermeiden.
Es können auch mehrere Objekte ein Speicherleck erzeugen. Wenn Sie den Button Retain
6.7.2 Ich folgte einem Zombie
Was passiert eigentlich, wenn Sie ein Objekt hinter einem Dangling Pointer weiterverwenden? Die Antwort ist nichts für schwache Nerven, lieber Leser, denn es entsteht ein Zombie,
der in Ihrem Speicher Angst und Schrecken verbreitet. Das Schlimme an Zombies ist, dass
das von ihnen angerichtete Unheil erst lange nach seiner Entstehung auftreten kann.
In Ihrer App entsteht ein Zombie, wenn Sie einen Verweis auf ein Objekt verwenden, nachdem Sie es freigegeben haben. Die Freigabe muss dabei indes nicht unbedingt über die gleiche Variable wie der Zombie-Zugriff erfolgen. Das erschwert natürlich die Suche nach diesen
Ungeheuern.
Objekt
1
1
markiert Instruments auch drei Zeilen anstatt einer (siehe Abbildung 6.56).
3
4
Zombie
2
1
Cycle in der Beispiel-App drücken, zeigt Ihnen Instruments drei Zeilen in der Übersicht an,
und wenn Sie sich über den Aufrufstapel den entsprechenden Quellcode anzeigen lassen,
2
2
3
4
Zombie
3
5
6
7
8
3
4
neues Objekt
9 Es gibt durchaus einige bekannte Speicherlecks in Cocoa Touch; die Wahrscheinlichkeit, dass Ihr Code den
Fehler verursacht, ist jedoch wesentlich höher.
746
Abbildung 6.57 Ein Zombie im Hauptspeicher
747
6
Models, Layer, Animationen
6.7
Was Sie schon immer über Instruments wissen wollten, aber nie zu fragen wagten
Abbildung 6.57 veranschaulicht eine Möglichkeit für einen Zombie im Hauptspeicher. Alles
beginnt mit einem Objekt, das seine Daten irgendwo im Hauptspeicher schön ordentlich
abgelegt hat 1. Irgendwann gibt das Programm den Speicherbereich dieses Objekts frei,
obwohl es noch Referenzen auf dieses Objekt hat. Dadurch hat es einen Zombie erzeugt, und
alle Verweise darauf sind Dangling Pointer 2. Der Zugriff auf dieses Objekt über die Dangling
Pointer muss indes jetzt noch nicht zu einem Fehler oder Absturz führen, da der Speicherbereich ja in der Regel immer noch die Daten des ursprünglichen Objekts enthält, was die
Abbildung durch graue Ziffern darstellt.
Jedes Mal, wenn das Programm danach ein neues Objekt anlegt, besteht jedoch die Möglichkeit, dass dieses neue Objekt den Speicherbereich des Zombies oder Teile davon belegt und
überschreibt 3. Diese Situation kann unterschiedliche Fehler hervorrufen. Das Programm
kann sich unvorhergesehen verhalten oder abstürzen. In der Regel bemerken Sie erst in dieser Situation, dass Ihr Programm einen Zombie enthält.
Die Beispiel-App Instruments erlaubt Ihnen auch die Erzeugung von Zombies. Sie brauchen
dabei keine Angst zu haben. Sie machen das ja unter Laborbedingungen, und da kann Ihnen
nichts passieren – na ja, fast nichts. Starten Sie die App über den Profile-Button aus Xcode
in Instruments, und wählen Sie die Schablone Zombies im Auswahldialog aus.
Abbildung 6.59 Instruments hat einen Zombie entdeckt.
Wenn Sie den Pfeil neben der Speicheradresse anklicken, gelangen Sie zu einer Auflistung
der Speicherverwaltungsoperationen, die das Programm auf dem Objekt ausgeführt hat.
Wenn Sie den Button By Time aktivieren, listet Instruments die Speicherverwaltungsereignisse ihrer zeitlichen Abfolge nach auf (siehe Abbildung 6.60). Auch hier können Sie wieder
die unproblematischen Ereignisse über den Button Unpaired ausblenden, was Sie allerdings bei diesem Beispiel nicht machen sollten.
Abbildung 6.60 Speicherverwaltungsoperationen eines Zombies
Die letzte Zeile enthält die Methode oder Funktion, die auf den Zombie zugegriffen hat. Es ist
die Systemfunktion _NSDescriptionWithLocaleFunc, auf deren Quellcode Sie keinen Zugriff
haben. Diese Funktion hat zwar auf den Zombie zugegriffen, sie hat ihn jedoch nicht erzeugt.
Der Zombie ist durch das Release in der vorletzten Zeile entstanden, wie Sie an dem Referenzzähler sehen. Da dieses Release jedoch vom Autoreleasepool gesendet wurde, kann
Abbildung 6.58 Auswahl der Schablone für Zombies
diese Zeile auch nicht direkt für den Fehler verantwortlich sein. Der Verursacher ist entweder
das Autorelease der zweiten oder das Release der drittletzten Zeile.
Nach dem Start zeigt Instruments Ihnen den Speicherverbrauch der App an. Wenn Sie zwei-
Auch hier können Sie sich über den Aufruf von View 폷 Inspectors 폷 Show Extended
mal auf den Button Object in der Rubrik Zombies der iOS-App klicken, hält die Ausführung
Detail oder (cmd)+(3) jeweils den Aufrufstapel zu den Anweisungen auf der linken Seite des
an, und Instruments zeigt einen Dialog wie in Abbildung 6.59.
Fensters anzeigen lassen. Der Stapel stellt alle Systemsymbole in grauer Schrift mit einem
748
749
6
Models, Layer, Animationen
6.7
Was Sie schon immer über Instruments wissen wollten, aber nie zu fragen wagten
farbigen Icon dar. Dagegen stellt er die Symbole aus Ihrem Programmcode in schwarzer
6.7.3 Time Bandits
Schrift mit einem schwarzen Icon dar. Abbildung 6.61 zeigt einen Ausschnitt eines Aufruf-
Instruments hilft Ihnen allerdings nicht nur beim Finden von Speicherverwaltungsfehlern,
sondern es kann auch andere Schwachstellen des Programms aufdecken. Zu den gesuchtesten Schwachstellen in Programmen gehören die Methoden und Funktionen, die Ihr Programm langsam und schwerfällig machen. Mit dem Messinstrument Time Profiler können
Sie wunderbar den Zeitverbrauch Ihrer Methoden und Funktionen messen.
stapels. Mit einem Doppelklick auf diese Symbole springen Sie an die entsprechende Stelle
des Quelltextes.
Je öfter Sie im Instruments-Beispielprogramm auf den Button Compute Sum drücken, umso
länger braucht die App für die Berechnung der angezeigten Werte. Anscheinend ist die
Implementierung also für größere Werte unzureichend. Um dem Zeitfresser auf die Spur zu
kommen, starten Sie das Programm in Instruments aus Xcode heraus. Im Auswahldialog
wählen Sie die Schablone Time Profiler aus (siehe Abbildung 6.62).
Abbildung 6.61 Detailansicht und Ausschnitt des Aufrufstapels
Da sich der Convenience-Konstruktor object an die Speicherverwaltungsregeln hält, bleibt
überraschenderweise nur die Methode makeZombie als Kandidat übrig.
- (IBAction)makeZombie {
id theZombie = [InstrumentsDemoObject object];
NSLog(@"zombies=%@", self.zombies);
[self.zombies addObject:theZombie];
[theZombie release];
}
Listing 6.140 Zombie-Erzeugung
Da die Methode object nicht auf die erste Speicherverwaltungsregel passt, hält die Variable
theZombie nicht das Objekt. Also darf sie das Objekt auch nicht freigeben. Der Fehler liegt also
Abbildung 6.62 Vorlage für Laufzeitanalysen auswählen
in der letzten Zeile der Methode, die ein Release an die Variable theZombie sendet.
Automatisches Referenzzählen und Zombies
Von einem ARC-Compiler übersetzter Code sollte in der Regel keine Zombies mehr erzeugen,
wenn Sie anstatt assign den Speicherverwaltungstyp weak in den Property-Deklarationen
verwenden. Sie können zwar auch mit eingeschaltetem ARC Zombies erzeugen, indem Sie beispielsweise Void-Zeiger oder den Modifizierer __unsafe_unretained verwenden. Diese Fälle
sollten in der Praxis allerdings selten auftreten.
750
Nach dem Start der App bleibt der untere Bereich zunächst leer. Wenn Sie einige Male den
Button Compute Sum drücken, füllt Instruments die Anzeige mit Funktions- und Methodennamen. Stellen Sie die Sortierung nach der ersten Spalte, Running Time, absteigend ein.
Sie zeigt Ihnen den absoluten und den relativen Zeitverbrauch für die Ausführung des jeweiligen Symbols. Wenn Sie auf der rechten Seite in der Rubrik Call Tree die Option Separate
by Thread ausschalten und die Option Top Functions einschalten, zeigt Instruments
wesentlich mehr Zeilen in der Übersicht an. Viele Zeilen enthalten jedoch C-Funktionen oder
Methoden aus den Systembibliotheken. Sie können diese Zeilen über die Option Hide System Libraries ausblenden. Durch diese Einstellungen sollte die Anzeige ungefähr so wie in
Abbildung 6.63 aussehen.
751
6
Models, Layer, Animationen
6.7
Was Sie schon immer über Instruments wissen wollten, aber nie zu fragen wagten
Anscheinend verbraucht die Methode sum ihre Rechenzeit fast komplett im Schleifenrumpf
für den Aufruf der Methode successorWithIndex: (siehe Abbildung 6.65). Instruments hebt
diese Zeile freundlicherweise rot hervor und unterstreicht den Methodennamen.
Abbildung 6.65 Dem Zeitverbrauch auf der Spur …
Abbildung 6.63 Gefilterter Aufrufstapel
An den relativen Zeiten (Prozentzahlen) sehen Sie, dass sich in der Übersicht einige Zeitfresser befinden. Beispielsweise hat das zweite Symbol von oben, die Methode computeSum in der
Klasse InstrumentsDemoObject, einen Zeitverbrauch von über 98 %. Anscheinend hat diese
Methode einen gehörigen Hunger auf die wertvolle Rechenzeit. Allerdings verbraucht diese
Methode nicht die Zeit für sich allein, sondern ruft andere Methoden auf, die ebenfalls sehr
zeitraubend sind. Sie können das überprüfen, indem Sie den Eintrag durch Anklicken des
Dreiecks neben dem Symbolnamen aufklappen.
Durch einen Doppelklick auf eine Zeile in der Übersicht zeigt Instruments den Quellcode des
Symbols an. Instruments hebt hierbei die Zeilen mit höherem Zeitverbrauch hervor und
zeigt den relativen Zeitverbrauch bezogen auf den Gesamtzeitverbrauch der Methode an
(siehe Abbildung 6.64). Falls dort keine Prozentzahlen stehen, können Sie die Ansicht über
das Zahnradsymbol rechts über dem Quelltext anpassen. Wählen Sie dort den Punkt View
as Percentage aus.
Abbildung 6.64 Zeitverbrauch der Anweisungen in einer Methode
Durch Anklicken des grauen Info-Symbols auf der rechten Seite können Sie sich den Aufrufstapel für diese Zeile ansehen. Dort finden Sie als unterstes Symbol die Methode sum, und
Sie gelangen durch einen Doppelklick auf dieses Symbol zu dieser Methode. Auf diese Weise
können Sie sich immer weiter durch die Methoden des Programms hangeln, um vielleicht
den Zeitfresser zu finden.
752
Wenn Sie den Methodennamen anklicken, gelangen Sie zum Quelltext dieser Methode.
Auch hier zeigt Instruments Ihnen wieder den relativen Zeitverbrauch der Zeilen in Prozent
an. Diese Methode verbraucht den größten Teil ihrer Rechenzeit für den Aufruf des Getters
successor. Dabei hängen die genauen Zahlen natürlich davon ab, wie oft Sie den Button
gedrückt haben.
Abbildung 6.66 … die anscheinend im Nichts endet
Da es sich um eine synthetisierte Property handelt, sieht das zunächst nach einer Sackgasse
aus. Ein einzelner lesender Property-Aufruf verbraucht natürlich nicht so viel Zeit. Allerdings
befindet er sich in einer Schleife, genau wie die Methode successorWithIndex:. Die Anzahl der
Schritte der inneren Schleife hängt von dem Parameter inIndex ab. Dessen Wert ist der Schleifenindex der äußeren Schleife. Wenn die äußere Schleife bis 8 läuft, dann ruft die App successorWithIndex: insgesamt achtmal auf und liest die Property 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 = 28-mal
aus. Bei einer Länge von 1.000 kommen schon 500.500 Zugriffe zustande, und bei 8.000 sind
es gar über 32 Millionen. Das verbraucht natürlich auch einiges an Rechenzeit.
Zur Behebung dieses Performance-Leaks ist also eine Neuimplementierung von sum notwendig. Anstatt auf die verkettete Liste über einen Index zuzugreifen, sollte sich die Schleife lieber über einen Zeiger durch die Liste hangeln:
- (NSUInteger)sum {
NSUInteger theSum = 0;
InstrumentsDemoObject *theItem = self;
while(theItem) {
753
6
Models, Layer, Animationen
theSum += theItem.counter;
theItem = theItem.successor;
}
return theItem;
}
Listing 6.141 Effizientere Implementierung der Summenberechnung
Dieses Verfahren besucht jedes Listenelement nur einmal. Bei einer Listenlänge von 8.000
Einträgen liest diese Implementierung den Nachfolger auch nur 8.000-mal aus.
Das leidige Thema Arbeit
Instruments kann Ihnen nur Hinweise auf die Schwachstellen in Ihren Programmen liefern. Die
Behebung überlässt es großzügigerweise Ihnen ganz allein. Für die Suche nach Speicherverwaltungsfehlern kommen Sie also trotz Instruments nicht um die Kenntnis der Regeln herum.
Die Zeiträuber in Ihrer App zu suchen und zu beseitigen ist da schon komplizierter. Zwar liefert
der Time Profiler Ihnen hier gute Hinweise; doch die Probleme zu lösen ist unter Umständen
sehr schwierig oder gar unmöglich. Es muss ja schließlich nicht immer ein effizienteres Verfahren für die entsprechende Aufgabe geben.
6.7.4 Instruments und der Analyzer
Einige Schwachstellen des Beispielprojekts Instruments lassen sich auch ohne Instruments
über den Analyzer finden. Wenn Sie für das Projekt den Menüpunkt Product 폷 Analyze aufrufen, zeigt Ihnen Xcode zwei Schwachstellen des Programms an, die Sie auch mit Hilfe von
Instruments gefunden haben (siehe Abbildung 6.67).
Abbildung 6.67 Schwachstellen über den Analyzer finden
Die Bedienung des Analyzers ist zwar einfacher, und er beschreibt auch das Problem häufig
viel besser, als Instruments das vermag. Allerdings findet er bei weitem nicht so viele
Schwachstellen wie Instruments. Beispielsweise bleiben der Retain-Zyklus und Attribute mit
Speicherlecks oder Zombies dem Analyzer verborgen, und auch die Zeitbanditen lassen sich
nur mit Instruments genauer untersuchen.
754