1.1 Voraussetzungen

Transcription

1.1 Voraussetzungen
Kapitel 1
Hello iPhone
1
»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 ein 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.
21
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 Mountain Lion (10.8) oder Mavericks (10.9), 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 5 und läuft nur unter Mountain Lion
oder Mavericks. 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.
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. Neben ausreichend Hauptspeicher
(2 GB sind ein guter Anfang) sollte Ihr Mac über ein großes Display verfügen. Für größere Projekte und längeres Arbeiten sollten Sie mindestens ein 15"-Display verwenden, um nicht zu schnell die Übersicht zu verlieren.
22
1.1
1.1.1
Voraussetzungen
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,– EUR 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.
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
23
1
1
Hello iPhone
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 Mountain Lion arbeiten, können dort auch noch eine Version für Lion oder Snow Leopard 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 Mountain Lion auch in
Zukunft mit neuen SDKs versorgt. Aktualisieren Sie im Zweifelsfall also lieber auf
OS X 10.9, sofern das möglich ist; damit haben Sie immer Zugriff auf die neueste SDKVersion.
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. Die Voraussetzungen dafür sind
lediglich die kostenlose Anmeldung im Apple Developer Network und das Herunterladen und Installieren des jeweils aktuellen SDK oder die Installation über den Mac
App Store.
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 Host-Computers, 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.
24
1.1
Voraussetzungen
1
Abbildung 1.2 Der iPhone-Simulator
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.
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 dafür:
왘
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.
25
1
Hello iPhone
왘
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 Hardwareeigenschaften. Sie können keine
Fotos mit der Kamera schießen, und es gibt keinen Beschleunigungs- oder Gyrosensor.
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.
1.1.3
Test der Arbeitsumgebung
Xcode hat eine eigene Versionsnummerierung, deren aktuelle Version 5 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.3 Das Startfenster von Xcode
26
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
(siehe Abbildung 1.4 und Abbildung 1.5); eine Wahl haben Sie ohnehin nicht.
Abbildung 1.4 Installation der Command Line Tools
Abbildung 1.5 Installation zusätzlicher Komponenten
Das SDK bringt standardmäßig immer nur die Unterstützung für die aktuelle iOSVersion mit. Bei Bedarf können Sie über den Menüpunkt Xcode • Preferences •
Downloads die Debug-Unterstützung und den Simulator für ältere iOS-Versionen
von Apple laden. Sie starten den Download, indem Sie jeweils auf das Symbol rechts
neben der Größenangabe (siehe Abbildung 1.6) klicken.
Xcode unterwegs
Da Sie neben der Xcode-Installation aus dem App Store eben noch diverse Zusatzpakete nebst Offlinedokumentation 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.
27
1
1
Hello iPhone
Abbildung 1.6 Unterstützung für ältere iOS-Versionen
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 (siehe
Abbildung 1.3) 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 (siehe Abbildung 1.7).
Abbildung 1.7 Ein neues iOS-Projekt
28
1.1
Voraussetzungen
Durch Auswahl des Buttons Next gelangen Sie zu dem in Abbildung 1.8 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.
Abbildung 1.8 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.
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.8 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 deren 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 App-Hersteller zu vermeiden.
29
1
1
Hello iPhone
Zusätzliche Einstellungen bei älteren Xcode-Versionen
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.
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.9).
Abbildung 1.9 Das Hauptfenster von Xcode
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.10).
30
1.1
Voraussetzungen
1
Abbildung 1.10 Auswahl des Ausführungsziels
Xcode zeigt im Infobereich oben in der Mitte des Fensters den aktuellen Fortschritt
an (siehe Abbildung 1.11) und startet anschließend das Projekt im iPhone-Simulator.
Abbildung 1.11 Fortschrittsanzeige von Xcode
Wenn Sie im Simulator eine langweilige, weiße Fläche wie in Abbildung 1.12 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.
Abbildung 1.12 Das Testprojekt funktioniert.
31
1
Hello iPhone
1.2
App geht’s
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.
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.
Projektinformation
Den Quellcode des folgenden Beispielprojekts finden Sie auf der beiliegenden DVD
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_3 erreichen und
dort den kompletten Inhalt in einer Zip-Datei laden.
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.13 gezeigt.
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.14).
5 In Xcode 4 heißt die Datei MainStoryboard.storyboard.
6 Graphical User Interface
32
1.2
App geht’s
1
Abbildung 1.13 Projekteinstellungen für die Beispiel-App
Abbildung 1.14 Die grafische Benutzerschnittstelle der neuen App
33
1
Hello iPhone
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.15). Ein Storyboard kann beliebig viele solcher Szenen enthalten.
Abbildung 1.15 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.16 gezeigten Button klicken.
34
1.2
App geht’s
1
Abbildung 1.16 Öffnen des Utilities-Bereiches
Unten rechts in der Utilities-Ansicht finden Sie die Objektbibliothek, indem Sie das
Würfelsymbol auswählen (siehe Abbildung 1.17).
Abbildung 1.17 Die Objektbibliothek
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 eine Beschreibung dazu (siehe Abbildung 1.18).
Abbildung 1.18 Label mit Beschreibung in der Objektbibliothek
Ziehen Sie ein Label mit der Maus oben links in den View, so wie es Abbildung 1.19
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.
35
1
Hello iPhone
Abbildung 1.19 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.20).
Abbildung 1.20 Größenänderung des Labels
Danach setzen Sie die Textausrichtung des Labels im Attributinspektor auf zentriert
(siehe Abbildung 1.21). Sie können den Attributinspektor öffnen, indem Sie das Label
im View auswählen und die Tastenkombination (alt)+(cmd)+(4) drücken.
36
1.2
App geht’s
1
Abbildung 1.21 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.22) 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.22 Buttons in der Objektbibliothek
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 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.23. Die in dieser Abbildung gezeigten blauen Rahmen um
den 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.
37
1
Hello iPhone
Abbildung 1.23 Der View mit Label und Button
Ü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.24) im Simulator. Das Drücken des Buttons verändert zwar dessen
Farbe, ansonsten tut sich aber nichts.
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 der URL anzeigt. Dazu müssen Sie einige Zeilen Code programmieren und
den Button und das Label mit diesem Code verbinden.
38
1.2
App geht’s
1
Abbildung 1.24 Die App im Simulator
1.2.2
Sehr verbunden
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 Headerdateien und deklarieren verschiedene Programmierelemente. Im Gegensatz dazu heißen Dateien mit der Endung .m Implementierungsdateien. Sie enthalten den eigentlichen Programmcode. Die Headerdatei des Viewcontrollers beschreibt also, was diese Klasse kann, und die Implementierungsdatei beschreibt, wie sie es macht.
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-
39
1
Hello iPhone
ß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 Pop-up-Menü (Abbildung
1.26). Darin wählen Sie den Eintrag ViewController.h aus, so dass der Hilfseditor
diese Datei anzeigt.
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 können,
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).
40
1.2
App geht’s
1
Abbildung 1.27 Haupt- und Hilfseditor
Abbildung 1.28 Verbindung vom Label zur Deklaration erstellen
41
1
Hello iPhone
Nach dem Loslassen erscheint der in Abbildung 1.29 gezeigte Popup-Dialog, in dem Sie
die Art der anzulegenden Verbindung definieren. Eine Verbindung von einem ViewElement 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.
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.30 Eine Verbindung vom Button zum Code
42
1.2
App geht’s
Lassen Sie den Mausknopf unter der Deklaration der Property los, und stellen Sie in
dem sich öffnenden Popup-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.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
Durch den Aufruf der Funktion NSLog veranlassen Sie die App, die Zeichenkette
[+] go: in die Konsole zu schreiben.
Ü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
43
1
1
Hello iPhone
Button in der App. Xcode öffnet automatisch am unteren Rand den Debug-Bereich,
und darin erscheint die Zeichenkette mit einem Zeitstempel davor wie in Abbildung
1.32. Das Drücken des Buttons führt also zum Aufruf der vorgesehenen Methode.
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.
- (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];
if(theError == nil) {
NSLog(@"[+] IP: %@", theIP);
}
else {
44
1.2
App geht’s
NSLog(@"[+] Error: %@", [theError localizedDescription]);
}
[self.label setText:theIP];
1
}
Listing 1.3 Abfrage einer Webseite
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.
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).
Abbildung 1.33 Es klappt – die App zeigt die IP-Adresse an.
45
1
Hello iPhone
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.
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.
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).
46
1.2
App geht’s
1
Abbildung 1.34 Auswahl eines nicht näher spezifizierten iOS-Gerätes
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, schließen Sie es an Ihren Computer
an, öffnen Xcode und darin den Organizer über den Menüpunkt Window | Organizer. Im Tab Devices sehen Sie links iPhones und iPads, die Sie mit Xcode bereits verwendet haben. Wählen Sie das Gerät aus, das Sie verwenden wollen. Im Hauptfenster
sehen Sie einige Angaben zum Gerät und den wichtigen Button mit der Beschriftung
Use for Development.
Abbildung 1.36 Ein noch nicht für die Entwicklung eingerichtetes iPhone
47
1
Hello iPhone
Durch Drücken dieses Buttons weisen Sie Xcode an, das betreffende Gerät für die Entwicklung einzurichten. Bei älteren Versionen von Xcode war dies noch nicht so
einfach möglich, dort musste man als Entwickler erst umständlich mit dem Schlüsselbund eine Zertifizierungsanfrage erstellen und dann im Entwicklerportal von
Apple ein Entwicklerzertifikat beantragen.
Für die Einrichtung muss Xcode zunächst wissen, ob Sie sich bereits als Entwickler
registriert haben und einem entsprechenden Programm beigetreten sind. Dazu zeigt
es den in Abbildung 1.37 gezeigten Dialog an. Falls Sie bereits Mitglied in einem der
drei Programme sind, können Sie den Button Add... drücken. Der Button Join a Program... öffnet eine Webseite in Ihrem Browser, über die Sie sich über die möglichen
Programme informieren können.
Abbildung 1.37 Haben Sie bereits einen Entwicklerzugang?
Wenn Sie den Button Add... drücken, fragt Sie Xcode als Nächstes nach den Zugangsdaten für das Entwicklerportal, also nach der Apple ID, mit der Sie sich als zahlender
iOS-Entwickler registriert haben. Dieses Konto richten Sie in den Einstellungen von
Xcode unter Accounts ein; zu den Einstellungen gelangen Sie übrigens auch über
den Menüpunkt Xcode | Preferences... (siehe Abbildung 1.38).
Abbildung 1.38 Die Account-Einstellungen von Xcode
48
1.2
App geht’s
Wenn Sie danach wieder in den Organizer wechseln, sehen Sie den Dialog aus Abbildung 1.39, über den Sie das Entwicklungsteam auswählen. Wählen Sie hier mindestens ein Team aus, und drücken Sie anschließend den Button Choose.
Abbildung 1.39 Das Konto für das iOS Developer Program
Xcode prüft anschließend, ob bereits ein Entwicklerzertifikat im Portal vorhanden
ist. Beim ersten Durchlauf dieses Vorgangs findet Xcode natürlich kein Zertifikat und
fragt über den Dialog aus Abbildung 1.40, ob es den Vorgang zum Erstellen eines Zertifikates anstoßen soll.
Abbildung 1.40 Xcode möchte ein Zertifikat anfordern.
Starten Sie die Anfrage über den Request-Button. Xcode erstellt anschließend im
Hintergrund einen Certificate Signing Request und schickt ihn an das Entwicklerportal, um ein Zertifikat zu beantragen. Schließlich installiert es das Zertifikat und versetzt Sie damit in die Lage, Ihre eigenen Apps auf Ihrem Gerät auszuführen. Öffnen
Sie nach Abschluss des Vorgangs erneut den Xcode-Organizer, und wählen Sie links
das zum Test vorgesehene, angeschlossene Gerät aus.
Abbildung 1.41 Das Gerät ist betriebsbereit.
49
1
1
Hello iPhone
Sie sehen an der grünen Markierung in der Seitenleiste, dass das Gerät für die Arbeit
mit Xcode bereit ist. Wechseln Sie zurück in das Hauptfenster, und wählen Sie oben
in der Werkzeugleiste das Schema für das aktuelle Projekt aus. Neben den diversen
Simulatoren sehen Sie dort in dem Auswahlfeld nun auch das betreffende Gerät.
Wählen Sie es aus, und klicken Sie auf Run; nach der Übersetzung startet die App auf
dem Gerät (siehe Abbildung 1.42).
Abbildung 1.42 Gerät für die Ausführung auswählen
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 (siehe Abbildung 1.43).
Abbildung 1.43 Der Weg zum iOS Provisioning Portal
7 https://developer.apple.com/devcenter/ios/index.action
50
1.2
App geht’s
Die direkte URL des Portals lautet https://developer.apple.com/ios/manage/
overview/index.action.
Im iOS Provisioning Portal finden Sie in der linken Navigationsspalte den Punkt Certificates (siehe Abbildung 1.44). Über diesen Punkt gelangen Sie zur Zertifikatsverwaltung. Sie können dort Ihr Zertifikat manuell herunterladen oder widerrufen.
Abbildung 1.44 Das Zertifikat im Portal
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 davon. 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 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
51
1
1
Hello iPhone
Das Ergebnis ist eine Darstellung der im Zertifikat gespeicherten Informationen wie
in Abbildung 1.45.
Abbildung 1.45 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 App-IDs 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.46).
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.47).
52
1.2
App geht’s
1
Abbildung 1.46 Mögliche Berechtigungen für eine App
Abbildung 1.47 Das Entwicklerkonto für die Berechtigungsoperation
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.48 Aktivierung der Datenverschlüsselung
53
1
Hello iPhone
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.2.7
Profilprobleme
Es kann vorkommen, dass sich Xcode weigert, Ihre App auf Ihrem Gerät zu installieren und zu starten. Xcode bricht den Build-Prozess mit einer Fehlermeldung – einem
Code-Sign-Fehler – ab. Diese Meldung kann unterschiedliche Gründe haben.
Als Erstes sollten Sie überprüfen, ob Xcode auch das richtige Zertifikat verwendet.
Öffnen Sie dazu im Target den Reiter Build Settings, und geben Sie im Suchfeld den
Begriff »Code Signing« ein. Unter Code Signing Identity sollte das Target für die
Konfiguration Debug den Schlüssel Any iOS SDK und den Wert iPhone Developer
enthalten (siehe Abbildung 1.49).
Abbildung 1.49 Anzeige des Entwicklungszertifikats
Danach öffnen Sie im Organizer den Reiter Devices und darunter den Menüpunkt
Provisioning Profiles. Sie sehen hier in einer Tabelle alle Profile, die Xcode zur
Verfügung stehen. Entwicklungsprofile haben nur eine begrenzte Gültigkeitsdauer.
Das Verfallsdatum zeigt Ihnen die mittlere Spalte Expiration Date der Tabelle an.
Ein abgelaufenes Profil hat zudem in der Spalte Status den Eintrag Profile has
expired mit einem roten Ausrufezeichen davor (siehe Abbildung 1.50).
54
1.3
Zusammenfassung
1
Abbildung 1.50 Profile mit unterschiedlichen Zuständen im Organizer
Bei abgelaufenen Profilen finden Sie einen Button Renew, mit dem Sie das Profil
aktualisieren können. Schließlich sollten Sie noch die Profile auf dem Gerät überprüfen. Im Organizer finden Sie unter dem Gerät, wenn es angeschlossen ist, den Menüpunkt Provisioning Profiles. Darüber können Sie die installierten Profile auf dem
Gerät ansehen und auch gegebenenfalls über den Minus-Button löschen. Alternativ
können Sie sie aber auch in den Einstellungen Ihres Gerätes unter Allgemein •
Profil(e) ansehen. Sie sollten unbedingt abgelaufene Profile von Ihren Geräten
löschen, da iOS sie manchmal gegenüber den aktuellen Profilen bevorzugt.
Abbildung 1.51 Anzeige der Profile auf dem iPhone
1.3
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.
55
1
Hello iPhone
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). Dabei beschreibt die Headerdatei (.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
Benutzeroberfläche verbinden.
왘
Über die Methode viewWillAppear: können Sie vor der Anzeige eines Views automatisch Aktionen ausführen.
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.
56
Kapitel 6
Models, Layer, Animationen
»Ach, er will doch nur spielen.«
– Unbekannter Hundebesitzer
6
Animationen sind ein wichtiger, jedoch leider häufig auch unterschätzter Bestandteil einer grafischen Benutzerschnittstelle. Durch Animationen können Sie die Aktionen der Applikation hervorheben und so dem Nutzer eine zusätzliche
Rückmeldung geben.
Eine gute Animation hebt die Veränderungen auf dem Bildschirm hervor und verlängert den Wahrnehmungszeitraum für den Nutzer, ohne dabei störend zu wirken.
Wenn Sie beispielsweise in der Tabellenansicht des Fototagebuchs einen Eintrag auswählen, dann schiebt der Navigationcontroller die Detailansicht auf den Bildschirm.
Diese Animation hebt einerseits den Viewwechsel hervor. Sie erklärt andererseits
auch den Zurück-Button in der Detailansicht: Sie sind durch eine Bewegung nach
rechts in diese Ansicht gelangt. Also gelangen Sie mit dem Pfeil nach links wieder
zurück.
Sie können hingegen Animationen nicht nur für den Wechsel kompletter Screens
verwenden, sondern sie auch auf einzelne Views und deren Darstellungsschicht, den
Layern, anwenden. In diesem Kapitel lernen Sie Layer und die verschiedenen Animationsmöglichkeiten von Cocoa Touch kennen.
Projektinformation
Den Quellcode des folgenden Beispielprojekts Games finden Sie auf der DVD unter
Code/Apps/iOS7/Games oder im Github-Repository zum Buch im Unterverzeichnis
https://github.com/Cocoaneheads/iPhone/tree/Auflage_3/Apps/iOS7/Games.
Das Beispielprojekt Games dieses Kapitels enthält zwei einfache Spiele, an denen sich
die Funktionsweise von Animationen besonders gut verdeutlichen lässt. Die Spiele
kennen Sie wahrscheinlich. Das erste ist ein Schiebepuzzle, bei dem Sie Bildteile auf
einer quadratischen Fläche so lange verschieben müssen, bis die Teile zu einem
Gesamtbild verschmelzen. Bei dem zweiten Spiel handelt es sich um das bekannte
Memory-Spiel.
569
6
Models, Layer, Animationen
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.
6.1
Modell und Controller
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.
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 es auch die Konsistenz der Spieledaten sicherstellen, und das ist komplizierter als bei vielen Core-Data-Datenmodellen. Das Modell des Schiebepuzzles
stellt die Gültigkeit sicher, indem es nur erlaubte Operationen auf den Daten zulässt.
Die Klasse Puzzle im Projekt Games stellt das Modell des Schiebepuzzles dar. Sie verwendet dazu ein C-Array von NSUInteger-Werten. Dabei stellt jeder Wert ein Puzzleteil dar, während die Position eines Wertes im Array die Position des entsprechenden
Puzzleteils im Spielfeld angibt.
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
570
6.1
Modell und Controller
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].
6
Konsistenz des Schiebepuzzles
Das Modell eines Schiebepuzzles ist konsistent, wenn sich die Anordnung der Werte
in dessen Array durch beliebige Schiebeoperationen aus der Ausgangsstellung
erzeugen lässt. Die Puzzledarstellung [0, 1, 2, 3, 4, 15, 6, 7, 8, 5, 9, 11, 12, 13, 10, 14] (rechtes Bild in Abbildung 6.1) ist also konsistent, da sie sich aus der Ausgangsdarstellung
erzeugen lässt.
Ein mögliches Beispiel für ein inkonsistentes Puzzle hat das Array [1, 0, 2, 3, 4, 5, 6, 7,
8, 9, 10, 11, 12, 13, 14, 15]. Das ist ein Puzzle in der Ausgangsstellung, bei dem die ersten beiden Teile vertauscht sind. Sie können die Teile eines konsistenten Puzzles
beliebig oft verschieben, jedoch nie diese Anordnung der Teile erreichen.
Um die Konsistenz des Puzzlemodells sicherzustellen, liegen ihm folgende Regeln
zugrunde:
1. Ein neues Puzzle hat immer die Ausgangsstellung.
2. Alle Methoden, die die Anordnung der Teile verändern, basieren auf erlaubten
Spielzügen.
3. Alle anderen Methoden lesen die Daten nur aus oder basieren auf Methoden der
zweiten Regel.
Oder mit anderen Worten: Die Operationen im Modell entsprechen immer genau
den Operationen des wirklichen Schiebepuzzles.
Das Puzzle besitzt eine private Property items, die das Array mit den Werten enthält,
und die Länge des Arrays speichert das Modell in der Property size. Die erste Regel
lässt sich sehr einfach herstellen. Wenn die Klasse das Array anlegt, setzt sie alle Einträge entsprechend:
NSUInteger theSize = self.size;
for(NSUInteger i = 0; i < theSize; ++i) {
self.items[i] = i;
}
self.freeIndex = theSize – 1;
Listing 6.1 Initialisierung des Arrays des Modells
571
6
Models, Layer, Animationen
Im Beispielprogramm finden Sie diese Schleife in der internen Methode clear des
Puzzlemodells. Das Puzzle merkt sich in der Property freeIndex außerdem den Index
des freien Feldes im Array. Das ist zwar nicht unbedingt notwendig, erleichtert
jedoch die Implementierung der anderen Methoden.
Die Puzzleklasse besitzt zwei Methoden, mit denen Sie die Anordnung der Puzzleteile
verändern können. Die beiden Methoden bilden die Steuerungsmöglichkeiten des
Spiels ab. Sie können das iPhone in vier Richtungen kippen, um die Teile zu bewegen.
Diese Steuerungsmöglichkeit implementiert die Methode tiltToDirection:. Sie können außerdem einen Stein berühren und ihn auf das freie Feld ziehen, was die
Methode moveItemAtIndex:toDirection: abbildet.
Für die Richtungen verwendet das Modell einen eigenen Aufzählungstyp PuzzleDirection mit fünf möglichen Werten; für jeweils jede Richtung einen. Mit dem Wert
PuzzleNoDirection lassen sich auch Zustände ohne spezifische Richtung abbilden.
typedef enum {
PuzzleDirectionUp = 0,
PuzzleDirectionRight,
PuzzleDirectionDown,
PuzzleDirectionLeft,
PuzzleNoDirection
} PuzzleDirection;
Listing 6.2 Aufzählungstyp mit den möglichen Bewegungsrichtungen
Abbildung 6.2 stellt die möglichen Spielzüge des Feldes mit dem Index 6 dar. Das
freie Feld befindet sich dabei jeweils in dem Feld, auf das der Pfeil zeigt. Wenn Sie beispielsweise das Puzzle nach oben kippen, dann muss das freie Feld den Index 2
haben. Oder andersherum: Wenn Sie das Puzzle nach oben kippen und der Index des
freien Feldes ist 2, dann muss die Methode tiltToDirection: die Felder 2 und 6 miteinander vertauschen.
Daraus können Sie, ausgehend vom freien Feld an der Position freeIndex, die Regeln
für das Kippen herleiten:
Kipprichtung
Index des Feldes für den Tausch
links
freeIndex + 1
rechts
freeIndex - 1
oben
freeIndex + 4
unten
freeIndex - 4
Tabelle 6.1 Regeln für das Kippen des Puzzles
572
6.1
0
1
2
3
Modell und Controller
Oben
Rechts
4
5
6
7
8
9
10
11
Links
Unten
6
Right
12
13
14
15
Abbildung 6.2 Spielzüge im Puzzle
Es gibt natürlich auch ungültige Züge. Nehmen wir an, das freie Feld befindet sich an
Position 12, und Sie kippen das Puzzle nach rechts. Nach den Regeln aus Tabelle 6.1
müssten Sie dann die Felder 11 und 12 miteinander vertauschen (gestrichelter Pfeil in
Abbildung 6.2). Das ist dennoch kein gültiger Zug, weil der Stein dabei die Zeile und
Spalte auf einmal wechselt. Bei einem gültigen Zug müssen also die Indizes des freien
Feldes und des Tauschfeldes entweder in der gleichen Zeile oder in der gleichen
Spalte liegen.
Den Zeilen- oder Spaltenindex zu einem Feldindex können Sie über eine Division mit
Rest mit 4 als Teiler ermitteln. Dazu ein paar Beispiele: Wenn Sie 13 durch 4 mit Rest
teilen, erhalten Sie 13 = 3 × 4 + 1 also 3 mit Rest 1 als Ergebnis, und 5 = 1 × 4 + 1 ist 1 mit
Rest 1. Da der Divisionsrest bei beiden Rechnungen gleich ist, liegen beide Werte in
der gleichen Spalte. Hingegen ist 15 = 3 × 4 + 3 also 3 Rest 3. Die Divisionsreste von 13
und 15 sind zwar unterschiedlich, allerdings ist bei beiden Werten der Quotient 3.
Also liegen diese Zahlen in der gleichen Zeile. Wenn Sie das nicht glauben, dann
schauen Sie doch in Abbildung 6.2.
Außerdem kann es bei der Anwendung der Regeln aus Tabelle 6.1 passieren, dass der
berechnete Index nicht zwischen 0 und 15 liegt. Da das Modell für Indexwerte vorzeichenlose Zahlen vom Typ NSUInteger verwendet, können bei einer Subtraktion
jedoch keine negativen Zahlen entstehen. Stattdessen findet ein Überlauf statt – mit
einer sehr großen Zahl als Ergebnis. Wenn die Applikation beispielsweise 4 von 3
abzieht, ist das Ergebnis 4.294.967.295. Die Gültigkeit eines Kippzuges des Puzzles
überprüft die Klasse Puzzle anhand der beiden Methoden rowOfFreeIndexIsEqualToRowOfIndex: und columnOfFreeIndexIsEqualToColumnOfIndex:, die Sie in Listing 6.3
sehen.
573
6
Models, Layer, Animationen
- (BOOL)rowOfFreeIndexIsEqualToRowOfIndex:
(NSUInteger)inToIndex {
NSUInteger theLength = self.length;
NSUInteger theSize = self.size;
NSUInteger theIndex = self.freeIndex;
return inToIndex < theSize &&
(theIndex / theLength) == (inToIndex / theLength);
}
- (BOOL)columnOfIndexFreeIndexIsEqualColumnOfIndex:
(NSUInteger)inToIndex {
NSUInteger theLength = self.length;
NSUInteger theSize = self.size;
NSUInteger theIndex = self.freeIndex;
return inToIndex < theSize &&
(theIndex % theLength) == (inToIndex % theLength);
}
Listing 6.3 Gültigkeitsprüfung für Spielzüge
Dabei enthält der Parameter inIndex die Position des Feldes für die Vertauschung mit
dem leeren Feld. Der Ausdruck self.length liefert die Breite beziehungsweise Höhe
des Puzzles – also 4. Wenn eine von beiden Methoden aus Listing 6.3 den Wert YES liefert, vertauscht die Methode tiltToDirection: die Werte im angegebenen Feld und
im freien Feld.
Die Logik der Methode moveItemAtIndex:toDirection: ist verglichen mit tiltToDirection: wesentlich einfacher. Der Indexparameter gibt das Feld für die Vertauschung mit dem leeren Feld an. Sie brauchen also nur zu prüfen, ob das leere Feld in
der angegebenen Richtung vom angegebenen Feld liegt. Dazu berechnet die
Methode den Index des Feldes in der angegebenen Richtung analog zur Methode
tiltToDirection:. Wenn dieser Wert mit dem angegebenen Index übereinstimmt,
vertauscht die Methode die Werte der beiden Felder.
Das Modell speichert neben den Positionen der Puzzleteile auch die Anzahl der
durchgeführten Züge. Dazu stellt die Klasse die nur lesbare Property moveCount zur
Verfügung und erhöht den dahinterstehenden Wert bei jedem gültigen Zug in moveItemAtIndex:toDirection: und tiltToDirection: um eins.
Mit der Methode shuffle können Sie das Puzzle durchschütteln. Sie ermittelt dazu
mehrmals über die Methode nextIndex eine zufällige Position, auf die sie dann das
freie Feld verschiebt. Das iPhone kann zwar keine echten Zufallszahlen erzeugen,
574
6.1
Modell und Controller
allerdings sind die Rückgabewerte der Systemfunktion rand() so schön durcheinandergewirbelt, dass sich der jeweils nächste Wert vom Nutzer nur schwer vorhersagen
lässt. Aus diesem Grund nennt man diese Werte auch Pseudozufallszahlen. Die Funktion liefert jedoch nach jedem Programmstart immer die gleiche Zahlenfolge, was
beim Puzzle immer zu der gleichen Stellung führen würde. Die App löst dieses Problem, indem Sie in der Methode applicationDidBecomeActive die Funktion srand()
mit der aktuellen Uhrzeit über die Anweisung
srand((unsigned) [NSDate timeIntervalSinceReferenceDate]);
6
aufruft. Dadurch ändert sie jeweils den Anfangszustand für die Berechnung der Pseudozufallszahlen auf einen anderen Wert. Die Methode nextIndex ermittelt so lange
ein neues Feld, bis dessen Index ungleich dem Index des freien Feldes ist (siehe Listing 6.4).
- (NSUInteger)nextIndex {
NSUInteger theSize = self.size;
NSUInteger theIndex = rand() % theSize;
while(theIndex == self.freeIndex) {
theIndex = rand() % theSize;
}
return theIndex;
}
Listing 6.4 Berechnung der nächsten Position für die Methode »shuffle«
Die Methode shuffle verwendet die Methode tiltDirectionForIndex: aus Listing 6.5,
die zu einer Position eine Kipprichtung berechnet, die das leere Feld näher zum Feld
mit der angegebenen Position schiebt. Die Implementierung dieser Methode muss
mehrere Fälle überprüfen. Bei wiederholten Aufrufen liefert sie zunächst so lange
vertikale Richtungen, bis das freie Feld und die Position in einer Zeile liegen, und
danach liefert sie horizontale Richtungen, bis beide Positionen übereinstimmen.
Falls die übergebene Position schon mit der Position des freien Feldes übereinstimmt, liefert die Methode tiltDirectionForIndex: den Wert PuzzleNoDirection
zurück, um anzuzeigen, dass keine Feldvertauschung notwendig ist.
- (PuzzleDirection)tiltDirectionForIndex:(NSUInteger)inIndex {
NSUInteger theFreeIndex = self.freeIndex;
if(inIndex == theFreeIndex) {
return PuzzleNoDirection;
}
else if([self rowOfFreeIndexIsEqualToRowOfIndex:
575
6
Models, Layer, Animationen
inIndex]) {
return inIndex < theFreeIndex ?
PuzzleDirectionRight : PuzzleDirectionLeft;
}
else {
return inIndex < theFreeIndex ?
PuzzleDirectionDown : PuzzleDirectionUp;
}
}
Listing 6.5 Berechnung einer Kipprichtung zu einer Position
Diese wiederholten Aufrufe führt die Methode shuffle aus. Sie ermittelt zunächst
eine Position über nextIndex und schiebt danach das freie Feld durch mehrfache Aufrufe der Methode tiltToDirection: auf das Feld mit diesem Index. Das macht sie
mehrmals, so dass das Puzzle danach schön durcheinandergewürfelt, aber trotzdem
lösbar ist. Dabei ist die Lösbarkeit dadurch garantiert, dass dieses Vorgehen nur
erlaubte Operationen nach den oben genannten Regeln ausführt. Die Methode shuffle erhält also die Konsistenz des Puzzles.
- (void)shuffle {
NSUInteger theSize = self.size;
for(NSUInteger i = 0; i < 4 * theSize; ++i) {
NSUInteger theShuffleIndex = self.nextIndex;
PuzzleDirection theDirection =
[self tiltDirectionForIndex:theShuffleIndex];
while(theDirection != PuzzleNoDirection) {
[self tiltToDirection:theDirection
withCountOffset:0];
theDirection = [self
tiltDirectionForIndex:theShuffleIndex];
}
}
self.moveCount = 0;
}
Listing 6.6 Schütteln des Puzzles
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-
576
6.1
Modell und Controller
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
[thePuzzle moveItemAtIndex:theIndex
toDirection:inDirection];
}
- (void)handleLeftSwipe:
(UISwipeGestureRecognizer *)inRecognizer {
[self handleGestureRecognizer:inRecognizer
withDirection:PuzzleDirectionLeft];
}
- (void)handleRightSwipe:
(UISwipeGestureRecognizer *)inRecognizer {
[self handleGestureRecognizer:inRecognizer
withDirection:PuzzleDirectionRight];
}
- (void)handleUpSwipe:
(UISwipeGestureRecognizer *)inRecognizer {
[self handleGestureRecognizer:inRecognizer
withDirection:PuzzleDirectionUp];
}
577
6
Models, Layer, Animationen
- (void)handleDownSwipe:
(UISwipeGestureRecognizer *)inRecognizer {
[self handleGestureRecognizer:inRecognizer
withDirection:PuzzleDirectionDown];
}
Listing 6.7 Auswertung der Swipe-Gesten
Die Methode handleGestureRecognizer:withDirection: ermittelt zunächst über die
Methode locationInView: des Gesture-Recognizers die Koordinaten der Berührung
im Puzzleview, und aus den Koordinaten bestimmt sie die Zeile und die Spalte des
Felde im Puzzle. Aus diesen beiden Werten kann sie dann den entsprechenden Index
des Feldes berechnen. Mit der übergebenen Richtung ruft sie dann die Methode
moveItemAtIndex:toDirection: auf.
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-Motion-Framework über die Target-Einstellungen einbinden. Dazu
klicken Sie unter Linked Frameworks and Libraries den Plus-Button an, wählen
den Eintrag CoreMotion.framework aus und drücken den Button Add (siehe
Abbildung 6.3).
Abbildung 6.3 Hinzufügen des Core-Motion-Frameworks
578
6.1
Modell und Controller
Apple empfiehlt, jeweils höchstens einen Motionmanager pro Applikation zu erzeugen. In der Games-App verwendet zwar nur das Puzzle dieses Objekt, dennoch ist es
eine gute Idee, es zentral über das App-Delegate zu verwalten, das dafür die Property
motionManager besitzt. Die Initialisierung erfolgt in der Methode application:didFinishLaunchingWithOptions:, die auch das Aktualisierungsintervall des Managers auf
eine Zehntelsekunde festlegt.
self.motionManager = [CMMotionManager new];
[self.motionManager setAccelerometerUpdateInterval:0.1];
6
Listing 6.8 Erzeugung und Initialisierung des Beschleunigungssensors
Die Kategorie UIViewController(Games) stellt über die Methode motionManager den
Motionmanger den Viewcontrollern zur Verfügung. Der Motionmanager liefert die
Werte des Beschleunigungssensors über den Aufruf eines Blocks an die Applikation,
die Sie beim Start an die Methode startAccelerometerUpdatesToQueue:withHandler:
als zweiten Parameter übergeben. Der erste Parameter ist eine Operationqueue, in
der der Funktionsaufruf erfolgt. Sie können hier einfach die Haupt-Queue verwenden.
- (void)viewDidAppear:(BOOL)inAnimated {
[super viewDidAppear:inAnimated];
CMMotionManager *theManager = self.motionManager;
[theManager startAccelerometerUpdatesToQueue:
[NSOperationQueue mainQueue]
withHandler:^(CMAccelerometerData *inData,
NSError *inError) {
if(inData == nil) {
NSLog(@"error: %@", inError);
}
else {
[self handleAcceleration:inData.acceleration];
}
}];
}
Listing 6.9 Starten der Beschleunigungssensorabfragen
Das Stoppen der Abfragen erfolgt in der Methode viewWillDisapear: über einen Aufruf der Methode stopAccelerometerUpdates.
- (void)viewWillDisappear:(BOOL)inAnimated {
CMMotionManager *theManager = self.motionManager;
579
6
Models, Layer, Animationen
[theManager stopAccelerometerUpdates];
[super viewWillDisappear:inAnimated];
}
Listing 6.10 Stoppen der Beschleunigungssensorabfragen
Der Beschleunigungssensor liefert die Werte in einem Objekt der Klasse CMAccelerometerData an die Funktion. Dieses Objekt liefert über die Property acceleration ein
Datum der Struktur CMAcceleration, das drei Fließkommawerte entlang der Hauptachsen x, y und z enthält (siehe Abbildung 6.4).
+y
-z
-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 = 0 –1. Halten Sie hingegen das Telefon wie
in Abbildung 6.4 genau senkrecht, beispielsweise um ein Foto zu schießen, dann
erhalten Sie die Werte x = 0, y = –1, z = 0. Es hat also immer diejenige Achse einen Wert
von +/–1, die nach unten zeigt, wobei das Vorzeichen dem Vorzeichen an der Achsenbeschriftung entspricht.
580
6.1
Modell und Controller
Do it yourself
Apple stellt das Beispielprogramm MotionGraphs mit der Dokumentation zur Verfügung. Damit können Sie sich die Werte des Beschleunigungssensors auf Ihrem
iPhone anzeigen lassen. Dieses Programm ist sehr praktisch, wenn Sie eigene Programme mit Beschleunigungssensor-Unterstützung entwickeln wollen.
Um es in Xcode zu öffnen, rufen Sie die Hilfe über den Menüpunkt 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 Simulator keinen Beschleunigungssensor besitzt und diesen auch nicht nachahmen kann. Über die Tabbar dieser
App können Sie die verschiedenen Sensoren des Motionmanagers auswählen; den
Beschleunigungssensor aktivieren Sie über den mittleren Reiter Accelerometer.
Die Messwerte des Beschleunigungssensors wertet die Methode handleAcceration:
folgendermaßen aus: Wenn Sie das Gerät aus der horizontalen Lage in eine Richtung
kippen, schiebt die App den passenden Stein auf das freie Feld. Danach müssen Sie
das Gerät erst wieder in die Ausgangslage bringen, um den nächsten Stein verschieben zu können. Um das zu verwirklichen, merkt sich der Controller die letzte
Kipprichtung in der privaten Property lastDirection. Neben den vier Richtungen für
oben, unten, links und rechts gibt es ja noch einen Wert für keine Richtung namens
PuzzleNoDirection. Nur wenn die letzte Kipprichtung diesen Wert hat, führt der Controller einen Spielzug aus.
- (void)handleAcceleration:(CMAcceleration)inAcceleration {
float theX = inAcceleration.x;
float theY = inAcceleration.y;
if(self.lastDirection == PuzzleNoDirection) {
Puzzle *thePuzzle = self.puzzle;
if(fabs(theX) > kHorizontalMaximalThreshold) {
self.lastDirection = theX < 0 ?
PuzzleDirectionLeft : PuzzleDirectionRight;
}
else if(fabs(theY) > kVerticalMaximalThreshold) {
self.lastDirection = theY < 0 ?
PuzzleDirectionDown : PuzzleDirectionUp;
}
[thePuzzle tiltToDirection:self.lastDirection];
581
6
Models, Layer, Animationen
}
else if(fabs(theX) < kHorizontalMinimalThreshold &&
fabs(theY) < kVerticalMinimalThreshold) {
self.lastDirection = PuzzleNoDirection;
}
}
Listing 6.11 Auswertung der Beschleunigungssensorwerte
Für die Auswertung sind nur die x- und y-Werte interessant. Sie lassen sich direkt in
Links/rechts- beziehungsweise Unten/oben-Bewegungen übersetzen. Abbildung 6.5
veranschaulicht diese Auswertung. Wenn der x- und der y-Wert im grauen Quadrat
in der Mitte liegen und somit das Gerät nicht weit genug gekippt wurde, dann setzt
die Methode den Property-Wert lastDirection auf PuzzleNoDirection. Die Methode
setzt den Property-Wert jeweils auf die Konstante, in deren Bereich sich der x- und
der y-Wert befindet. Außerdem sendet sie diese Richtung natürlich auch über die
Methode tiltToDirection: als Kippbewegung an das Puzzle. Nur bei dem weißen
Bereich um das graue Quadrat verändert die Methode den Property-Wert nicht.
PuzzleDirectionUp
y
0,2
PuzzleNoDirection
-0,5
-0,2
0,2
x
0,5
PuzzleDirectionDown
Abbildung 6.5 Auswertungsbereiche für Beschleunigungswerte
582
PuzzleDirectionRight
0,5
PuzzleDirectionLeft
6
6.1
6.1.4
Modell und Controller
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.
2. Sie können komplexere Veränderungen des Modells unter Umständen nur sehr
schlecht über dieses Vorgehen abbilden. Die Methode shuffle führt beispielsweise
sehr viele Vertauschungsoperationen durch.
3. Wenn nicht nur ein, sondern mehrere Controller das Modell verändern können,
können der Modellinhalt und die Viewdarstellung voneinander differieren.
Diese Probleme lassen sich vermeiden, wenn das Modell den View automatisch über
die Veränderungen benachrichtigt. Das Modell darf indes auf keinen Fall eine Abhängigkeit zum Controller oder View haben, weswegen Sie vom Modell nicht einfach auf
diese Schichten zugreifen können. Außerdem sollte ja das Modell beliebig viele Controller und Views über Zustandsänderungen informieren können.
Stattdessen kann auch das Modell bei jeder Veränderung entsprechende Benachrichtigungen versenden. Die vom Modellzustand abhängigen Viewcontroller lauschen
auf diese Benachrichtigungen und aktualisieren sich und den View entsprechend.
Das Modell des Schiebepuzzles versendet zwei Benachrichtigungen mit jeweils gleich
aufgebautem Info-Dictionary. Die Methode tiltToDirection: verschickt die Benachrichtigung kPuzzleDidTiltNotification, während moveItemAtIndex:toDirection: die
Benachrichtigung kPuzzleDidMoveNotification versendet. Das Directory userInfo in
der Benachrichtigung enthält dabei die folgenden Schlüssel:
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
1 Siehe http://de.wikipedia.org/wiki/Code_smells.
583
6
6
Models, Layer, Animationen
Die Werte in der Tabelle haben alle den Typ NSUInteger. Die muss sie jedoch in NSNumber-Objekte kapseln, um sie in einem NSDictionary verwenden zu können.
Abbildung 6.6 stellt das Vorgehen zur Aktualisierung des Views grafisch dar. Wenn
der Nutzer eine Eingabe macht, läuft die Verarbeitung vom View über den Viewcontroller ins Modell. Das Modell schickt dann eine Benachrichtigung, die genau den
umgekehrten Weg nimmt. Dieses Vorgehen erinnert ein bisschen an das Spielen
über Bande beim Billard und wirkt umständlich. Der Vorteil dabei ist jedoch, dass der
View keine Änderung des Modells verpassen kann. Wenn beispielsweise ein anderer
Controller – symbolisiert durch das Fragezeichen – das Modell verändert, benachrichtigt es immer den View. Der View passt sich also immer dem Modell an.
View
Eingabe
Aktualisierung
Controller
?
Auswertung der
Eingabe
Auswertung der
Benachrichtigung
Modell
Aktualisierung
Versand bei einer
Benachrichtigung
Abbildung 6.6 Aktualisierung des Views über Modellaktualisierungen
Das Modell speichert außerdem die Anzahl der Züge. Auch hier soll die Anzeige des
Spielstands automatisch bei einer Änderung erfolgen. Das Modell könnte hierzu
auch Benachrichtigungen verwenden. Da es hierbei jedoch um die Beobachtung
eines einzelnen Wertes geht, ist hierfür Key-Value-Observing (KVO) besser geeignet.
Key-Value-Observing hat gegenüber Benachrichtigungen den Vorteil, dass Sie dafür
nichts am Modell ändern müssen. Die Möglichkeit, Werte eines Objekts zu beobachten, ist bei den beobachteten Objekten in Cocoa sozusagen schon eingebaut. Sie müssen nur noch den Beobachter einrichten. Das machen Sie über die Methode
addObserver:forKeyPath:options:context:.
Der PuzzleViewController registriert sich beim Puzzlemodell als Beobachter für die
Property moveCount über den folgenden Aufruf:
584
6.1
Modell und Controller
[self.puzzle addObserver:self forKeyPath:@"moveCount"
options:0 context:nil];
Listing 6.12 Registrierung als Beobachter eines Wertes
Bei jeder Änderung der Property moveCount ruft dann Cocoa automatisch die
Methode observeValueForKeyPath:ofObject:change:context: des Beobachters auf.
Dabei enthalten die ersten beiden Parameter den Namen der beobachteten Eigenschaft (hier moveCount) beziehungsweise das beobachtete Objekt (also das Puzzle). Der
Parameter change enthält ein Dictionary mit verschiedenen Werten der Property. Sie
können darüber beispielsweise den Wert vor der Änderung ermitteln. Dazu müssen
Sie allerdings bei der Registrierung im Parameter options den Wert NSKeyValueObservingOptionOld angeben. Das Dictionary enthält diesen Wert dann unter dem Schlüssel NSKeyValueChangeOldKey.
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.
585
6
6
Models, Layer, Animationen
Die registrierten Methodenaufrufe verwaltet der Undo-Manager intern über einen
Stapel (Last-In-First-Out, kurz LIFO) oder auch Undo-Stack. Beispielsweise können Sie
damit folgendermaßen die Undo-Operation für einen Setter-Aufruf registrieren:
- (void)setTitle:(NSString *)inTitle {
if(title != inTitle) {
[self.undoManager registerUndoWithTarget:self
selector:@selector(setTitle:) object:title];
title = [inTitle copy];
}
}
Listing 6.13 Setter mit Undo-Manager
Der Setter registriert im Undo-Manager einen Setter-Aufruf mit dem alten PropertyWert. Sie können den Manager durch einen Aufruf der Methode undo dazu veranlassen, die zuletzt registrierte Undo-Operation auszuführen. Wenn das der Setter aus
Listing 6.13 war, dann ruft der Undo-Manager erneut diesen Setter auf. Hierbei übergibt er jedoch den alten Wert, so dass das Objekt wieder den Titel vor dem ersten Setter-Aufruf hat.
Zu dem Zeitpunkt, an dem der Setter die Undo-Operation aufruft, registriert er einen
neuen Methodenaufruf. Dadurch kommt jedoch der Undo-Stack durcheinander! Aus
dieser Not hat Apple eine Tugend gemacht. Während der Ausführung von Undo-Operationen zeichnet der Undo-Manager alle Methodenregistrierungen als Redo-Operationen auf. Eine Redo-Operation macht eine Undo-Anweisung rückgängig, und Sie
können sie durch die Methode redo im Undo-Manager ausführen. Der Undo-Manager verwaltet die Redo-Operationen über einen eigenen Stapel – den Redo-Stack.
In Abbildung 6.7 ist die Interaktion des Setters aus Listing 6.13 mit dem Undo-Manager abgebildet. Der dargestellte Ablauf entspricht dabei den folgenden Programmanweisungen:
[theObject setTitle:@"Neu"];
[theObject.undoManager undo];
[theObject.undoManager redo];
Listing 6.14 Programmablauf zu Abbildung 6.7
Die gestrichelten Pfeile in der Abbildung stellen die Registrierung der Undo- und
Redo-Operationen dar. Die durchgezogenen Pfeile sind mit der aufgerufenen
Methode des Undo-Managers beschriftet und zeigen die Herkunft des ausgeführten
Methodenaufrufs an.
586
6.1
Undo-Stack
Modell und Controller
Redo-Stack
title = @"Alt"
setTitle:@"Neu"
setTitle:@"Alt"
clear
6
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"
Undo-Stack
title = @"Neu"
Redo-Stack
setTitle:@"Alt"
Abbildung 6.7 Interaktion des Setters mit dem Undo-Manager
Um nun den Undo-Manager in das Puzzlemodell zu integrieren, muss jede Kippoperation jeweils die entsprechende Kippoperation in die Gegenrichtung beim
Undo-Manager registrieren. Allerdings erhöht die Methode tiltToDirection: den
Zugzähler dabei immer um eins. Sie brauchen also eine Methode, die wie tiltToDirection:, die Puzzleteile verschiebt, den Zugzähler dagegen um eins verringert.
Anstatt die Methode zu kopieren und abzuändern, verwendet das Puzzle die interne
Hilfsmethode tiltToDirection:withCountOffset:. Den Wert des zweiten Parameters
addiert die Methode zu dem Zugzähler. Sie können also hier den Wert 1 bei normalen
Spielzügen und –1 bei Undo-Operationen angeben.
Diese Methode können Sie hingegen nicht über die Methode registerUndoWithTarget:selector:object: beim Undo-Manager registrieren, da sie erstens zwei Parameter und zweitens einfache Datentypen verwendet. Es gibt allerdings eine weitere
587
6
Models, Layer, Animationen
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 InvocationObjekt das Invocation-Target und legt das Objekt auf den Undo- beziehungsweise
Redo-Stack. Das Ganze klingt sehr kompliziert, ist jedoch relativ einfach; eine Implementierung könnte beispielsweise folgendermaßen aussehen:
- (id)prepareWithInvocationTarget:(id)inTarget {
self.preparedTarget = inTarget;
return self;
}
- (void)forwardInvocation:(NSInvocation *)inoutInvocation {
[inoutInvocation setTarget:self.preparedTarget];
self.preparedTarget = nil;
// inoutInvoction auf Undo- oder Redo-Stack legen
}
Listing 6.16 Invocation-Erzeugung über Proxyaufruf
Mit der Methode tiltToDirection:withCountOffset: können Sie jetzt die vollständige Undo- und Redo-Funktionalität des Puzzles implementieren. Dabei erfolgt die
Registrierung der Undo-Operation folgendermaßen:
588
6.1
Modell und Controller
id theProxy = [self.undoManager
prepareWithInvocationTarget:self];
...
[theProxy tiltToDirection:theReverseDirection
withCountOffset:-inOffset];
Listing 6.17 Registrierung der Undo-Operation im Puzzle
2
6
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 UndoManagers 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.
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. 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.
2 Meistens machen die Web-Proxys im Gegensatz zum Entwurfsmuster jedoch einfach auch nur
viel Ärger.
589
6
Models, Layer, Animationen
Funktionstests
Sie sollten Ihre App vor der Veröffentlichung von mehreren anderen Nutzern testen
lassen. Tester finden häufig die erstaunlichsten Fehler in den Apps. Optimalerweise
lassen Sie Ihre App nicht nur von verschiedenen Personen, sondern auch auf verschiedenen Geräten überprüfen.
Ein guter Testplan kann dabei übrigens sehr hilfreich sein. Er beschreibt Anwendungsfälle Ihres Programms mit den erwarteten Ergebnissen. Solche Pläne sollten
Sie inkrementell erweitern; das heißt, Sie entwickeln aus erkannten Fehlern des Programms neue Testfälle, die ein Wiederauftreten dieser Fehler anzeigen.
Es ist bei komplexen Programmen inzwischen üblich, automatisierte Testverfahren
zu erstellen und regelmäßige Testläufe durchzuführen. Xcode 5 unterstützt die
Erstellung und Ausführung von Modul- oder auch Unit-Tests erheblich, indem es
beim Anlegen eines neuen Projekts automatisch ein Target für solche Tests anlegt.
Dieses Target enthält eine Klasse, im Beispielprojekt GamesTests, in die Sie Ihre Testmethoden schreiben können.
Sie können das Target für die Tests indes auch noch später erstellen oder in Ihrem
Programm auch mehrere Test-Targets anlegen. Wählen Sie dazu den Menüpunkt
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 Unit Testing Bundle.
Abbildung 6.8 Anlegen eines neuen Targets
590
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.
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
591
6
6
Models, Layer, Animationen
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.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
592
6.1
Modell und Controller
Werte einfacher Datentypen wie zum Beispiel int, NSUInteger oder double vergleichen. Allerdings findet dabei keine automatische Typumwandlung statt, und so
schlagen die Tests
XCTAssertEqual(1, 1U, @"Fehler"); // Falsch: int mit unsigned
XCTAssertEqual(1, 1.0, @"Fehler"); // Falsch: int mit double
immer fehl.
Im Gegensatz dazu vergleicht das Testmakro XCTAssertEqualObjects(linkes Objekt,
rechtes Objekt, Fehlermeldung, ...) die beiden Objekte über die Methode isEqual:.
@implementation GamesTests
- (void)setUp {
[super setUp];
self.puzzle = [Puzzle puzzleWithLength:4];
}
- (void)tearDown {
self.puzzle = nil;
[super tearDown];
}
- (void)testCreation {
XCTAssertTrue(self.puzzle.length == 4,
@"invalid length = %d", self.puzzle.length);
XCTAssertTrue(self.puzzle.size == 16,
@"invalid size = %d", self.puzzle.size);
for(NSUInteger i = 0; i < self.puzzle.size; ++i) {
NSUInteger theValue = [self.puzzle valueAtIndex:i];
XCTAssertEqual(theValue, i,
@"invalid value %d at index %d", theValue, i);
}
}
@end
Listing 6.18 Unit-Test für die Ausgangsstellung des Puzzles
Die Tests lassen sich in Xcode über den Run-Button in der Werkzeugleiste starten.
Klicken Sie dazu auf diesen Button, und halten Sie ihn gedrückt, bis ein Pop-overMenü erscheint. Darin wählen Sie den Punkt Test aus, um Ihre Tests zu starten (siehe
Abbildung 6.11).
593
6
6
Models, Layer, Animationen
Die Auswahl von Test ändert außerdem dauerhaft die Darstellung und Beschriftung
des Buttons. Für eine wiederholte Ausführung der Tests müssen Sie ihn dann nur
noch kurz anklicken, und durch einen langen Klick und entsprechende Auswahl aus
dem Menü wechseln Sie wieder zu Run zurück.
Abbildung 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
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.13 Anzeige und Auswahl der Test-Targets
594
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.
6
Abbildung 6.14 Auswahl eines Targets als Test
Xcode zeigt die nicht erfüllten Testbedingungen nach der Ausführung der Testmethode als rote Fehlermeldungen im Quelltext an. Das Symbol links neben dem
Methodenkopf gibt das Gesamttestergebnis für die Methode an. Es ist entweder ein
grüner Punkt bei fehlerfreier Ausführung der Tests oder wie in Abbildung 6.15 ein
roter Punkt mit einem weißen Kreuz. Sie können diese grünen und roten Symbole
auch anklicken, um die entsprechende Methode und somit die darin enthaltenen
Tests auszuführen.
Abbildung 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.
595
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.
596
6.1
Modell und Controller
Aus Fehlern lernen
Ihr Programm hat einen Fehler? Wenn Sie die Fehlersituation in einem Testfall
nachbilden, haben Sie eine gute Möglichkeit, den Fehler einfacher und schneller zu
analysieren. Sie können ihn mit der Testmethode sozusagen unter Laborbedingungen untersuchen. Die Testbedingungen müssen natürlich das richtige Verhalten des
Programms prüfen. Außerdem können Sie mit Hilfe der Testmethode prüfen, ob Sie
den Fehler aus Ihrem Code eliminiert haben. Denn Ihre Testmethode zeigt so lange
Fehler an, bis Ihr Code richtig funktioniert.
Die testgetriebene Softwareentwicklung geht sogar noch einen Schritt weiter. Bei
diesem Vorgehen erstellt der Programmierer immer zuerst die Tests, bevor er den
eigentlichen Programmcode erstellt. Er verändert dabei den Programmcode so
lange, bis er alle Tests erfüllt.
Die beschriebenen Tests gehen davon aus, dass der Nutzer nur gültige Züge macht.
Das sieht in der Praxis allerdings meistens anders aus. Auch diesen Fall sollten Sie in
den Testfällen berücksichtigen. Beispielsweise darf das Kippen eines Puzzles in der
Ausgangsstellung nach links das Puzzle nicht verändern. In diesem Fall muss die
Methode tiltToDirection: den Wert NO liefern.
- (void)testInvalidMoves {
self.puzzle = [Puzzle puzzleWithLength:4];
XCTAssertFalse([self.puzzle
tiltToDirection:PuzzleDirectionLeft], @"tilt left.");
XCTAssertNil(self.notification, @"notification sent");
XCTAssertTrue(self.puzzle.solved, @"puzzle not solved");
XCTAssertFalse([self.puzzle
tiltToDirection:PuzzleDirectionUp], @"tilt up.");
XCTAssertNil(self.notification, @"notification sent");
XCTAssertTrue(self.puzzle.solved, @"puzzle not solved");
}
Listing 6.20 Testen unerlaubter Züge
Für nicht ausgeführte Züge darf das Modell natürlich auch keine Benachrichtigungen
versenden. Um die Benachrichtigungen überprüfen zu können, registriert sich die
Testklasse beim Notificationcenter und speichert die gesendeten Benachrichtigungen in der Property notification. Mit dem Makro XCTAssertNil(Ausdruck, Fehlermeldung, ...) können Sie überprüfen, ob ein Ausdruck nil ist. Die Testmethode
verwendet es, um festzustellen, ob das Puzzle tatsächlich keine Benachrichtigung
versendet hat.
597
6
6
Models, Layer, Animationen
- (void)setUp {
[super setUp];
[[NSNotificationCenter defaultCenter]
addObserver:self selector:@selector(puzzleDidTilt:)
name:kPuzzleDidTiltNotification object:nil];
self.puzzle = [Puzzle puzzleWithLength:4];
self.notification = nil;
}
- (void)tearDown {
[[NSNotificationCenter defaultCenter]
removeObserver:self];
self.notification = nil;
self.puzzle = nil;
[super tearDown];
}
- (void)puzzleDidTilt:(NSNotification *)inNotification {
self.notification = inNotification;
}
Listing 6.21 Registrierung für Benachrichtigungen in der Testklasse
Natürlich sollten die Tests auch den erfolgreichen Versand überprüfen. Da die Überprüfung einer Benachrichtigung mehrere Tests an verschiedenen Stellen umfasst,
enthält die Testklasse dafür die Methode checkNotificationWithName:fromIndex:
toIndex:, die verschiedene Aspekte der Benachrichtigung testet (siehe Listing 6.22).
- (void)checkNotificationWithName:(NSString *)inName
fromIndex:(NSUInteger)inFromIndex
toIndex:(NSUInteger)inToIndex {
NSDictionary *theUserInfo = self.notification.userInfo;
NSUInteger theFromIndex = [[theUserInfo
valueForKey:kPuzzleFromIndexKey] unsignedIntValue];
NSUInteger theToIndex = [[theUserInfo
valueForKey:kPuzzleToIndexKey] unsignedIntValue];
XCTAssertNotNil(self.notification,
@"notification is nil");
XCTAssertNotNil(theUserInfo, @"userInfo is nil");
XCTAssertTrue(self.puzzle == self.notification.object,
@"invalid puzzle");
XCTAssertEqual(inName, self.notification.name,
@"invalid name %@ != %@", inName,
598
6.1
Modell und Controller
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;
}
6
Listing 6.22 Auswertung der Benachrichtigungen des Puzzles
Damit können Sie die Testmethode testComplexMove entsprechend erweitern:
XCTAssertTrue([self.puzzle
tiltToDirection:PuzzleDirectionRight],
@"Can't tilt right.");
[self checkNotificationWithName:kPuzzleDidTiltNotification
fromIndex:14 toIndex:15];
XCTAssertTrue([self.puzzle
tiltToDirection:PuzzleDirectionDown],
@"Can't tilt down.");
[self checkNotificationWithName:kPuzzleDidTiltNotification
fromIndex:10 toIndex:14];
XCTAssertTrue([self.puzzle
tiltToDirection:PuzzleDirectionRight],
@"Can't tilt right.");
[self checkNotificationWithName:kPuzzleDidTiltNotification
fromIndex:9 toIndex:10];
XCTAssertTrue([self.puzzle
tiltToDirection:PuzzleDirectionDown],
@"Can't tilt down.");
[self checkNotificationWithName:kPuzzleDidTiltNotification
fromIndex:5 toIndex:9];
Listing 6.23 Überprüfung der Benachrichtigungen
Weitere Testklassen
Natürlich können Sie das Test-Target auch um weitere Testklassen erweitern. Dazu
verwenden Sie den Menüpunkt New File... und die Vorlage Objective-C test case
class. Häufig implementiert man die Testfälle einer Klasse in jeweils einer eigenen
Testklasse.
599
6
Models, Layer, Animationen
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
Die zweite Methode, splitIntoSubimagesWithRows:columns:, der Kategorie zerlegt ein
Bild auf 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,
600
6.2
Als die Views das Laufen lernten
theSize.width / inColumns, theSize.height / inRows);
NSMutableArray *theResult = [NSMutableArray
arrayWithCapacity:inRows * inColumns];
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];
6
}
Listing 6.25 Aufteilen eines Bildes für das Puzzle
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];
theFromView.frame = [self frameForItemAtIndex:theToIndex];
theToView.frame = [self frameForItemAtIndex:theFromIndex];
}
Listing 6.26 Aktualisierung der Puzzleteile
601
6
Models, Layer, Animationen
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;
}];
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.
NSLayoutConstraint *theWidthConstraint = ...;
NSLayoutConstraint *theHeightConstraint = ...;
theWidthConstraint.constant *= 2.0;
602
6.2
Als die Views das Laufen lernten
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 Parametern:
[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.
Abbrechen von Animationen
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:
[theView.layer removeAllAnimations];
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.
603
6
6
Models, Layer, Animationen
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 C-Struktur 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ß.
CGAffineTransformMakeScale
Erstellt eine Skalierung über zwei Werte entlang der xund 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.
Tabelle 6.3 Erzeugung von Transformationsmatrizen
604
6.2
Als die Views das Laufen lernten
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);
605
6
6
Models, Layer, Animationen
}
}
completion:^(BOOL inFinished) {
for(Card *theCard in theCards) {
CardView *theView = [self.memoryView.subviews
objectAtIndex:theCard.index];
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:
606
6.2
Als die Views das Laufen lernten
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.
...EaseOut
startet mit einer konstanten Geschwindigkeit und verlangsamt sich am Ende.
...Linear
6
hat über den gesamten Verlauf die gleiche Geschwindigkeit.
Tabelle 6.4 Geschwindigkeitsverläufe in der Animation
Projektinformation
Das Beispielprojekt Animation, das Sie auf der beiliegenden DVD unter Code/Apps/
iOS6/Animation oder im Github-Repository zum Buch im Unterverzeichnis https://
github.com/Cocoaneheads/iPhone/tree/Auflage_3/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
3 Falls Sie sie nicht kennen: Drücken Sie einfach ein Icon im Springboard so lange, bis alle Icons zu
wackeln anfangen.
607
6
Models, Layer, Animationen
options:UIViewAnimationOptionRepeat |
UIViewAnimationOptionAutoreverse |
UIViewAnimationOptionCurveEaseInOut
animations:^{
theView.transform =
CGAffineTransformMakeRotation(theAngle);
}
completion:^(BOOL inFinished) {
theView.transform = CGAffineTransformIdentity;
}];
Listing 6.31 Wackelanimation wie beim Bearbeiten des Springboards
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:
608
6.2
Als die Views das Laufen lernten
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.
...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
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
Memory-Spiel 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:.
609
6
6
Models, Layer, Animationen
- (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;
}
- (void)showFrontSide:(BOOL)inShow withAnimationCompletion:
(void (^)(BOOL 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.
610
6.2
Als die Views das Laufen lernten
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.
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 Dauern der ersten beiden Animationen
errechnet.
[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.
611
6
6
Models, Layer, Animationen
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) {
[self showCardView:inShow atIndex:inIndex + 1];
if(inIndex == 0 && inShow) {
[self showCardView:NO atIndex:0];
}
}];
}
}
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.
612
6.3
6.3
Core Animation
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.
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 LayerKlasse 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.
613
6
6
Models, Layer, Animationen
+ (Class)layerClass {
return [PieLayer class];
}
Listing 6.35 Festlegen der Layer-Klasse eines Views
Projektinformation
Den Quellcode des Beispielprojekts Pie finden Sie auf der DVD unter Code/Apps/
iOS5/Pie oder im Github-Repository zum Buch im Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Auflage_3/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
Sublayer
Sublayer
Abbildung 6.16 View- und Layer-Hierarchie
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
614
6.3
Core Animation
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;
6
Listing 6.36 Erzeugung eines Layer-Rahmens
Core Animation und das UIKit
Wie Sie an Listing 6.36 sehen, verwenden Layer Core-Graphics-Farben und keine
UIColor-Objekte. 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: also 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 Delegateobjekt frei wählen und über die Property
delegate setzen.
Informelles Protokoll
Es gibt kein formales Protokoll für das Layer-Delegate. Die Deklaration befindet sich
hingegen in der Kategorie NSObject(CALayerDelegate) in der Headerdatei der LayerKlasse. Diese Art, Delegate-Methoden zu deklarieren, heißt auch informelles Protokoll, und Sie finden die Dokumentation dieser Methoden unter »CALayerDelegate
Informal Protocol Reference«.
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
615
6
Models, Layer, Animationen
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"];
theLayer.contents = theImage.CGImage;
}
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.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 Viewinhalt ü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.
Der dritte Weg zur Bereitstellung des Layer-Inhalts ist das Überschreiben der
Methode drawInContext: in einer Unterklasse von CALayer. Auch hier bekommen Sie
den Kontext zum Zeichnen als Parameter übergeben.
Das Beispielprojekt Pie auf der beiliegenden DVD 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:
616
6.3
Core Animation
- (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;
6
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.39 Zeichnen des Kreissegments im Layer
Abbildung 6.17 Das »Pie«-Beispielprogramm
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
617
6
Models, Layer, Animationen
ü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-ValueCoding-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
618
6.3
Core Animation
diese Methode den Rückgabetyp id hat, muss sie den Standardwert für die Property
part als Objekt liefern.
static NSString * const kPartKey = @"part";
+ (id)defaultValueForKey:(NSString *)inKey {
return [kPartKey isEqualToString:inKey] ?
@0.0f : [super defaultValueForKey:inKey];
}
6
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 Property-Namen 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
Layer-Klasse 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.
619
6
Models, Layer, Animationen
Durch die Implementierung der dynamischen Property und der drei Methoden
defaultValueForKey:, needsDisplayForKey: und drawInContext: zeigt der Layer nun
auch ein Tortenstück an.
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.
Abbildung 6.18 Ausgabe der Layer-App
Projektinformation
Den Quellcode des Beispielprojekts Layer finden Sie auf der DVD unter Code/Apps/
iOS5/Layer oder im Github-Repository zum Buch im Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Auflage_3/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 CG-
620
6.3
Core Animation
ColorRef-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 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:
CAGradientLayer *theLayer = [CAGradientLayer layer];
NSArray *theColors = @[(id)[UIColor redColor].CGColor,
(id)[UIColor greenColor].CGColor,
(id)[UIColor blueColor].CGColor];
621
6
6
Models, Layer, Animationen
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);
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
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 Scroll-Layers ü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 Scroll-Layer stellt dann einen Ausschnitt
des Inhalts dar, der dieses Rechteck enthält. Listing 6.44 legt einen Scroll-Layer an, der
einen Textlayer enthält.
622
6.3
Core Animation
CAScrollLayer *theLayer = [CAScrollLayer layer];
CATextLayer *theTextLayer = ...;
theLayer.frame = inFrame;
[theLayer addSublayer:theTextLayer];
theLayer.scrollMode = kCAScrollVertically;
Listing 6.44 Anlegen eines Scroll-Layers
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);
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
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.
623
6
6
Models, Layer, Animationen
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.
Abbildung 6.20 Verschiedene Layer mit Masken
624
6.3
Core Animation
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.
6
Abbildung 6.21 Gradienten-Layer mit Textlayer als Maske
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;
}
Listing 6.46 Hinzufügen eines Layers für den Hintergrund
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.
625
6
Models, Layer, Animationen
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:
- (NSArray *)normalColors {
return [NSArray arrayWithObjects:
(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, nil];
}
- (NSArray *)highligthedColors {
return [NSArray arrayWithObjects:
(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, nil];
}
Listing 6.47 Definition der Verlaufsfarben für den Hintergrund
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 {
626
6.3
Core Animation
self.backgroundLayer.colors = self.normalColors;
}
}
Listing 6.48 Änderung des Farbverlaufs beim Drücken des Buttons
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.
6.3.5
Spieglein, Spieglein an der Wand
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.
Abbildung 6.23 Beispiel für eine Spiegelung
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 LayerInhalts. Sie müssen dazu den Inhalt in ein Bild zeichnen. Die Klasse CALayer stellt für
627
6
6
Models, Layer, Animationen
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.
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.
- (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;
if(thePresentationLayer) {
theLayer = thePresentationLayer;
}
if([theScreen respondsToSelector:@selector(scale)]) {
CGFloat theScreenScale = [theScreen scale];
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;
}
Listing 6.49 Erzeugung eines gespiegelten Abbildes eines Views
628
6.3
Core Animation
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 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);
}
Listing 6.50 Zeichnen der Spiegelung mit einer Graustufenmaske
629
6
6
Models, Layer, Animationen
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.
- (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
630
6.3
Core Animation
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.
6
Abbildung 6.24 Spiegelung der Ziffern des Zugzählers
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
@implementation DigitLayer
@dynamic digit;
+ (id)defaultValueForKey:(NSString *)inKey {
return [inKey isEqualToString:kDigitKey] ?
[NSNumber numberWithFloat:0.0] :
[super defaultValueForKey:inKey];
}
+ (BOOL)needsDisplayForKey:(NSString *)inKey {
return [inKey isEqualToString:kDigitKey] ||
[super needsDisplayForKey:inKey];
}
Listing 6.53 Der Layer für die Darstellung der Ziffern
631
6
Models, Layer, Animationen
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.
9
0
1
2
3
4
5
6
7
8
9
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
9
0
1
2
3
4
5
6
sichtbarer
Bereich
7
8
9
0
Abbildung 6.25 Ziffernwechsel des Digitviews
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.
632
6.3
Core Animation
- (void)drawLayer:(CALayer *)inLayer
inContext:(CGContextRef)inContext {
CGRect theBounds = self.bounds;
CGSize theSize = theBounds.size;
float theDigit = [(DigitLayer *)inLayer digit];
UIFont *theFont = self.font;
CGSize theFontSize = [@"0" sizeWithFont:theFont];
CGFloat theX = (theSize.width – theFontSize.width) / 2.0;
CGFloat theY = (theFont.capHeight – theSize.height) / 2.0;
6
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);
CGContextShowTextAtPoint(inContext, theX, theY,
&theCharacter, 1);
theY += theSize.height;
}
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.
633
6
Models, Layer, Animationen
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, theHeight, 0.0);
}
@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.
634
6.3
Core Animation
Sowohl die Erzeugung als auch die Darstellung einer Spiegelung über ReplicationLayer ist also einfacher als der Weg über die Methode renderInContext:; allerdings
ist das Ergebnis auch nicht ganz so schön.
6
Abbildung 6.26 Spiegelungseffekt für Arme
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 Animationsverzeichnis 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.
635
6
Models, Layer, Animationen
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
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:
CABasicAnimation *theAnimation = [CABasicAnimation animation];
theLayer.opacity = 0.0;
theAnimation.fromValue = @1;
theAnimation.toValue = @0;
636
6.3
Core Animation
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 PropertyWerten. 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 PropertyWert
aktueller PropertyWert plus byValue
nil
nil
x
aktueller PropertyWert
toValue
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.
637
6
6
Models, Layer, Animationen
CABasicAnimation *theAnimation = [CABasicAnimation animation];
theAnimation.fromValue =
[NSValue valueWithCGPoint:CGPointMake(10.0, 10.0)];
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 Core-Graphics-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.
638
6.3
Core Animation
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.
kCATransitionReveal
Schiebt den bestehenden Layer hinaus und gibt dadurch
den neuen View frei.
6
Tabelle 6.7 Die Transitionstypen von Core Animation
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 LayerHierarchie 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.
639
6
Models, Layer, Animationen
CABasicAnimation *theAnimation =
[CABasicAnimation animationWithKeyPath:@"cornerRadius"];
CAAnimationGroup *theGroup = [CAAnimationGroup animation];
theAnimation.toValue = [NSNumber numberWithFloat:160.0];
theAnimation.duration = 4.0;
theGroup.duration = 1.0;
theGroup.animations = [NSArray arrayWithObject:theAnimation];
theGroup.repeatCount = 3.0;
[theLayer addAnimation:theGroup forKey:@"group"];
Listing 6.59 Erzeugung einer Animationsgruppe
6.3.7
Daumenkino
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.
Projektinformation
Den Quellcode des Beispielprojekts FlipBookAnimation finden Sie auf der DVD
unter Code/Apps/iOS5/FlipBookAnimation oder im Github-Repository zum Buch im
Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Auflage_3/
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);
640
6.3
Core Animation
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);
6
[theResult addObject:(__bridge_transfer id)theImage];
}
CFRelease(theSource);
return theResult;
}
Listing 6.60 Laden der Bildfolge für die Animation
Core Graphics und Objective-C
Core Foundation enthält viele Datentypen, deren zugehörige Funktionen sich analog zu den Methoden einer entsprechenden Foundation-Klasse verhalten.
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 ARCUmgebung 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, »Über diese Brücke musst du
gehen«, geht noch genauer auf die Toll-free Bridge und die Modifizierer ein.
Die Methode öffnet über die URL das GIF-Bild und liefert eine Referenz 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 UI-
641
6
Models, Layer, Animationen
Images, 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;
theAnimation.repeatCount = HUGE_VALF;
theAnimation.duration = 1.0;
[theLayer addAnimation:theAnimation forKey:@"contents"];
}
Listing 6.61 Ausführen der Bildfolge über eine Keyframe-Animation
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.
642
6.3
Core Animation
6
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. 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:
Terminating app due to uncaught exception 'CAAnimationImmutable', reason:
'attempting to modify read-only animation <CAKeyframeAnimation: 0x7546810>'.
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
643
6
Models, Layer, Animationen
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;
}
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.
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
644
6.3
Core Animation
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.
beginTime
duration
Animation
Zeitachse
timeOffset
Abbildung 6.28 Startzeitpunkt auf der Zeitachse
Der Timeoffset bietet also genau die Möglichkeit, die richtige Position im Animationsablauf auszuwählen, und Sie ändern die Methode pause wie folgt:
- (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.
645
6
6
Models, Layer, Animationen
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, 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 setzen (siehe Abbildung 6.28), und die Länge der Pause entspricht der Differenz aus der aktuellen Zeit und der Startzeit der Pause.
Listing 6.64 enthält den kompletten Code für die resume-Methode. Da die Methode
convertTime:fromLayer: für ihre Berechnungen auch den Wert der Property beginTime verwendet, ist es übrigens wichtig, diesen Wert vorher auf null zu setzen.
- (void)resume {
CFTimeInterval theTime = self.timeOffset;
self.speed = 1.0;
self.timeOffset = 0.0;
self.beginTime = 0.0;
theTime = [self convertTime:CACurrentMediaTime()
fromLayer:nil] – theTime;
self.beginTime = theTime;
}
Listing 6.64 ... und Fortfahren mit dem richtigen Animationszustand
6.3.9
Der View, der Layer, seine Animation und ihr Liebhaber
Mit einer Property-Animation können Sie allerdings nicht nur die Standard-Propertys der Layer, sondern auch beliebige eigene Propertys animieren. Dazu sollten Sie
646
6.3
Core Animation
diese Propertys analog zu den Propertys part und digit in den Klassen PieLayer
beziehungsweise DigitView anlegen, wie das Abschnitt 6.3.1, »Layer«, beschreibt. Hier
sind noch einmal kurz die Punkte, 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üssel part in das Animationsverzeichnis eines Pie-Layers einfügen. Core Animation erzeugt dann während der Animation die entsprechenden Zwischenwerte für
diese Property, so dass ein flüssiger Bewegungsablauf entsteht.
Bist du flüssig?
Wie Sie vielleicht schon in Abschnitt 6.3.8, »Relativitätstheorie«, 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.
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. Dafür wäre es naheliegend, die Erzeugung der Animation in
den Setter der Property des Views oder des Layers zu verschieben. Allerdings würden
Sie damit die Animationserzeugung fest mit dem View oder dem Layer verdrahten.
Core Animation stellt stattdessen für die Animationserzeugung einen eleganteren
Weg zur 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 CAAction implementiert. Die Property-Änderung besteht
dabei aus drei Schritten:
1. Zuerst übernimmt der Layer den neuen Wert.
647
6
6
Models, Layer, Animationen
2. Als Nächstes erzeugt er die Aktion.
3. Danach führt der Layer die Aktion aus.
Die Layer-Methode actionForKey: erzeugt die Aktionen des Layers. Wenn Sie die
Standardaktionen eines Layers verändern oder neue Aktionen hinzufügen wollen,
haben Sie mehrere Möglichkeiten:
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.
Das Protokoll CAAction besitzt nur eine Methode runActionForKey:object:arguments:, die der Layer im dritten Schritt aufruft. Dabei ist der Parameter object der
Layer, der die Aktion ausgelö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 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
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;
648
6.3
Core Animation
}
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
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:
649
6
6
Models, Layer, Animationen
[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) {
theView.digit = value % 10;
theValue /= 10;
}
value = inValue;
}
Listing 6.70 Aktualisierung der Ziffern
650
6.3
Core Animation
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.
6
- (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 =
theAnimation.fromValue
theAnimation.toValue =
theAnimation.byValue =
return theAnimation;
inKey;
= self.fromValue;
nil;
nil;
}
else {
return [super actionForLayer:inLayer forKey:inKey];
}
}
Listing 6.72 Erzeugung der Animation für den Digitview
651
6
Models, Layer, Animationen
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-Animation-Transaktion. 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
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
652
6.3
Core Animation
kCAMediaTimingFunctionEaseInEaseOut erreichen, dass die Animation langsam an-
und ausläuft.
1
2
3
4
5
6
Abbildung 6.30 Die verschiedenen Standardanimationsverläufe
Bei der Animationserzeugung müssen Sie die Transaktionsparameter allerdings
explizit übernehmen und beispielsweise Listing 6.66 folgendermaßen anpassen:
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 erfolgt.
[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];
Listing 6.75 Verschachtelte Transaktionen
653
6
Models, Layer, Animationen
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];
}
Listing 6.76 Standardanimationszeit für Layer-Property festlegen
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.
Ü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.
654
6.3
Core Animation
Im Gegensatz dazu legt die Property anchorPointZ die Lage des Layers in einem dreidimensionalen Raum fest (siehe Abbildung 6.31).
y
Layer
6
x
z
Abbildung 6.31 Dreidimensionales Koordinatensystem der Layer
Projektinformation
Auf der DVD finden Sie das Beispielprojekt Animation3D im Ordner Code/Apps/
iOS7/Animation3D oder im Github-Repository zum Buch im Unterverzeichnis
https://github.com/Cocoaneheads/iPhone/tree/Auflage_3/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.
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.
655
6
Models, Layer, Animationen
Die affine Transformation eines Layers beschreibt die C-Struktur CATransform3D, die
eine 4×4-Matrix 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 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
CATransform3DMakeRotation(M_PI / 4.0, 0.0, 0.0, 1.0)
eine 45°-Drehung um die z-Achse als Rotationsachse.
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 y-Achse 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).
656
6.3
Core Animation
Abbildung 6.32 Layer mit Perspektive
6
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
Convenience-Konstruktor valueWithCATransform3D: in ein NSValue-Objekt umwandeln müssen, weil die Animationen ja Objekte und keine C-Strukturen erwarten.
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"];
Listing 6.78 Dreidimensionale Layer-Animation
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
Vorsicht, der Inhalt dieses Kastens kann Spuren von Mathematik enthalten, und das
Lesen kann zu abstraktem Denken führen.
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:
657
6
Models, Layer, Animationen
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);
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:
CABasicAnimation *theAnimation = [CABasicAnimation animation];
theAnimation.fromValue = [NSNumber numberWithFloat:0.0];
theAnimation.toValue = [NSNumber numberWithFloat:M_PI / 2.0];
[theLayer addAnimation:theAnimation
forKey:@"transform.rotation.x"];
Listing 6.79 Dreidimensionale Animation über einen Animationspfad
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:
CABasicAnimation *theAnimation = [CABasicAnimation animation];
theAnimation.fromValue = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0],
[NSNumber numberWithFloat:0.0],
[NSNumber numberWithFloat:0.0], nil];
theAnimation.toValue = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.5],
[NSNumber numberWithFloat:0.5],
[NSNumber numberWithFloat:0.5], nil];
[theLayer addAnimation:theAnimation forKey:@"transform.scale"];
Listing 6.80 Animierte Skalierung entlang mehrerer Achsen
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
658
6.4
Scrollviews und gekachelte Layer
Views nur einen Ausschnitt an, der auf die Fläche des Scrollviews passt. Der Nutzer
kann diesen Ausschnitt durch Wischbewegungen verändern. Scrollviews unterstützen außerdem das Zoomen des Inhalts. Dabei bewegen Sie zwei Finger aufeinander
zu, um den Inhalt zu verkleinern und somit den angezeigten 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
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 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 linken oberen Ecke des Scrollviews. Dabei misst sich dieser
Abstand von der Ecke des Scrollviews zu der Ecke der Fläche, so dass die beiden Koordinatenwerte immer positiv sind. Abbildung 6.33 stellt den Content-Offset und die
Content-Size durch einen beziehungsweise zwei Pfeile dar.
Inhaltsfläche
contentOffset
Scrollview
contentSize
contentInsets
Abbildung 6.33 Inhaltsfläche des Scrollviews
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 Inhalt legen, dessen Breite Sie über den Größeninspektor des Interface
659
6
Models, Layer, Animationen
Builders festlegen können. Falls Sie die Werte per Programmcode erzeugen wollen,
können Sie über die Funktion UIEdgeInsetsMake die notwendige C-Struktur anlegen.
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;
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);
}
Listing 6.81 Kleinen Inhalt im Scrollview zentrieren
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.
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.
660
6.4
Scrollviews und gekachelte Layer
Projektinformation
Den Quellcode des folgenden Beispiels finden Sie auf der DVD unter Code/Apps/
iOS5/ScrollView oder im Github-Repository zum Buch im Unterverzeichnis https://
github.com/Cocoaneheads/iPhone/tree/Auflage_3/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).
Abbildung 6.34 Aufbau des Views für das Scrollview-Beispiel
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
661
6
6
Models, Layer, Animationen
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 Interface Builder 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 eigenes Bild verwenden, sehen Sie
seine Größe im Dateiinspektor dieses Bildes in der Rubrik Image Properties.
Nach dem 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 Content-Size 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:
- (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
662
6.4
Scrollviews und gekachelte Layer
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>
6
@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
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
viewForZoomingInScrollView: 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 Con-
663
6
Models, Layer, Animationen
tainerview können Sie beispielsweise ein Objekt der Klasse UIView verwenden. Über
diesen View können Sie auch einfach die richtige Content-Size bestimmen, wie Sie
in Listing 6.82 gesehen haben.
Scrollviews und Autolayout
Falls Sie Scrollviews zusammen mit Autolayout verwenden möchten, müssen Sie
abwarten, bis das Autolayout die Größen und Positionen der Views berechnet hat,
um die Content-Size zu setzen. Die Methode viewWillAppear: im Beispielprojekt ist
dafür allerdings zu früh, da anscheinend bei eingeschaltetem Autolayout Cocoa
Touch die Content-Size danach noch einmal auf (0, 0) setzt.
Allerdings können Sie sich vom Viewcontroller informieren lassen, wenn sein View
das Layout abgeschlossen hat. Dazu überschreiben Sie die Methode viewDidLayoutSubviews; Listing 6.85 zeigt die entsprechende Implementierung.
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
CGSize theSize = self.contentView.frame.size;
self.scrollView.contentSize = theSize;
}
Listing 6.85 Aktualisieren der Content-Size bei Autolayout
Sie finden dazu auch ein entsprechendes Beispielprojekt unter Code/Apps/iOS7/
AutolayoutScrollView auf der beiliegenden DVD beziehungsweise unter https://
github.com/Cocoaneheads/iPhone/tree/Auflage_3/Apps/iOS7/
AutolayoutScrollView im Github-Repository.
6.4.2
Die Eventverarbeitung
Sie können jetzt in Ihrem 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
664
6.4
Scrollviews und gekachelte Layer
fest, dass der Recognizer erst bei zwei aufeinanderfolgenden Taps seine ActionMethode aufruft, die Sie als Nächstes anlegen.
Ziehen Sie dazu eine Verbindung vom Recognizer-Objekt in den Deklarationsblock in
der Headerdatei 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
- (IBAction)zoomIn:(id)inRecognizer {
CGFloat theScale = self.scrollView.zoomScale;
[self.scrollView setZoomScale:theScale * 2.0
animated:YES];
}
Listing 6.86 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 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.
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.
665
6
Models, Layer, Animationen
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.
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.35.
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.
Der kleinere View bekommt die Klasse LineView, deren Deklaration und Implementierung Sie in Listing 6.87 beziehungsweise in Listing 6.88 finden.
@interface LineView : UIView
@property(nonatomic) CGPoint startPoint;
@property(nonatomic) CGPoint endPoint;
@end
Listing 6.87 Deklaration der Klasse »LineView«
666
6.4
Scrollviews und gekachelte Layer
userInteractionEnabled
in Contentview
NO
YES
touchesShouldBegin:
withEvent:
inContentView:
6
NO
YES
canCancelContentTouches
YES
NO
NO
touchesShouldCancel
InContentView:
YES
Ereignisverarbeitung
durch Contentview
touchesCancelled:
withEvent:
an Contentview senden
Ereignisverarbeitung
durch Scrollview
Abbildung 6.35 Ereigniszuordnung des Scrollviews
@implementation LineView
@synthesize startPoint;
@synthesize endPoint;
- (void)drawRect:(CGRect)inRect {
UIBezierPath *thePath = [UIBezierPath bezierPath];
[[UIColor redColor] setStroke];
[thePath moveToPoint:self.startPoint];
[thePath addLineToPoint:self.endPoint];
667
6
Models, Layer, Animationen
[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
Listing 6.88 Implementierung der Klasse »LineView«
Die Views dieser Klasse zeichnen jeweils eine rote Linie vom Start- zum Endpunkt
einer Geste. Sie verbinden also den Punkt der ersten Berührung des Views mit dem
Punkt, an dem der Nutzer den Finger wieder vom Bildschirm genommen hat. Weisen
Sie diese Klasse dem 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.36.
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 Scrollbewegungen umzusetzen. Das können Sie leicht
nachvollziehen, wenn Sie den Entscheidungsweg für diese Einstellung in Abbildung
6.35 verfolgen.
668
6.4
Scrollviews und gekachelte Layer
6
Abbildung 6.36 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:
669
6
Models, Layer, Animationen
@implementation UIView(ScrollView)
- (BOOL)shouldCancelContentTouches {
return YES;
}
@end
@implementation ScrollView
- (BOOL)touchesShouldCancelInContentView:(UIView *)inView {
return [inView shouldCancelContentTouches] &&
[super touchesShouldCancelInContentView:inView];
}
@end
Listing 6.89 Abbruch der Gestenverarbeitung über die Views
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 Touch-Events an den Contentview weiter.
670
6.4
6.4.3
Scrollviews und gekachelte Layer
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 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.37 Linie mit 1 und ohne 2 Antialiasing sowie pixelgenau 3
In Abbildung 6.37 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];
CGContextSetAllowsAntialiasing(
UIGraphicsGetCurrentContext(), NO);
[[UIColor redColor] setStroke];
[thePath moveToPoint:self.startPoint];
[thePath addLineToPoint:self.endPoint];
[thePath stroke];
}
Listing 6.90 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 Dar-
671
6
6
Models, Layer, Animationen
stellung sollte vielmehr Bild 3 in Abbildung 6.37 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.
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 Layer in jeder Dimension 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 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.91 zeigt den Beginn der Implementierung dieser Klasse.
#import "LineView.h"
#import <QuartzCore/QuartzCore.h>
@implementation LineView
+ (id)layerClass {
return [CATiledLayer class];
}
Listing 6.91 Verwendung von »CATiledLayer« als Layer-Klasse
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.
672
6.4
Scrollviews und gekachelte Layer
- (void)awakeFromNib {
[super awakeFromNib];
CATiledLayer *theLayer = (CATiledLayer *)self.layer;
theLayer.levelsOfDetailBias = 2;
}
Listing 6.92 Einstellung der Detailgenauigkeit des Layers
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
Skalierungfaktor
Auflösung
0
4
800 × 800
1
2
400 × 400
2
1
200 × 200
3
1/2
100 × 100
4
1/4
50 × 50
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.
673
6
6
Models, Layer, Animationen
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.
6.4.4
Ganz großes Kino
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 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 4 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.38. 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.
Abbildung 6.38 Unterteilung der Zeichenfläche in Rechtecke
674
6.4
Scrollviews und gekachelte Layer
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.
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 Layer-Klasse fest und initialisiert die Kachelgröße in der Methode awakeFromNib.
6
const static CGFloat kImageWidth = 320.0;
const static CGFloat kImageHeight = 170.0;
+ (Class)layerClass {
return [CATiledLayer class];
}
- (void)awakeFromNib {
[super awakeFromNib];
CATiledLayer *theLayer = (CATiledLayer *)self.layer;
theLayer.tileSize = CGSizeMake(kImageWidth, kImageHeight);
}
Listing 6.93 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.
In Listing 6.94 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.94 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.
675
6
Models, Layer, Animationen
- (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;
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];
[theImage drawAtPoint:theTileFrame.origin];
}
}
}
}
Listing 6.94 Zeichnen der Kacheln
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 Log-Ausgabe sehen Sie außerdem, dass der Layer
die Rechtecke nicht von oben links nach unten rechts zeichnet.
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.94 sehr
(die Änderungen sind hervorgehoben):
676
6.4
Scrollviews und gekachelte Layer
- (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;
6
if(CGRectIntersectsRect(theRect, theTileFrame)) {
NSString *theFile = [NSString
stringWithFormat:@"flower_%ux%u",
4 – i, j];
NSString *thePath = [theBundle
pathForResource:theFile ofType:@"jpg"];
UIImage *theImage = [UIImage
imageWithContentsOfFile:thePath];
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);
677
6
Models, Layer, Animationen
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.39). 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.
Abbildung 6.39 Der Tiled Layer bei der Arbeit
6.4.5
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-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 auf der DVD unter Code/Apps/
iOS5/PDFView oder im Github-Repository zum Buch im Unterverzeichnis https://
github.com/Cocoaneheads/iPhone/tree/Auflage_3/Apps/iOS5/PDFView.
Dabei gibt die Methode drawPage:inRect:context: in Listing 6.95 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 CoreGraphics-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;
678
6.4
Scrollviews und gekachelte Layer
CGAffineTransform theTransform =
CGPDFPageGetDrawingTransform(inPage, kCGPDFMediaBox,
inRect, 0, YES);
CGContextConcatCTM(inContext, theTransform);
CGContextDrawPDFPage(inContext, inPage);
}
Listing 6.95 Zeichnen einer einzelnen PDF-Seite
6
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.
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.96 Variablendefinitionen für das Zeichnen der PDF-Seiten
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 Para-
679
6
Models, Layer, Animationen
meterwerte 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.97 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.97 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.40 aus. Diese Transformation veranschaulicht auch Abbildung
6.41. Der View spiegelt sich im Grafikkontext mit der Seite 1 an der horizontalen
Achse, und dadurch entsteht die gespiegelte Anzeige im View 2.
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.
680
6.4
Scrollviews und gekachelte Layer
6
Abbildung 6.40 Bei Core Graphics steht die Welt kopf.
View
2
Spiegelung
1
Spiegelung
Verschiebung
4
3
View
Abbildung 6.41 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.41 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.98 implementiert diese Korrektur der Koordinaten.
681
6
Models, Layer, Animationen
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.98 Rahmen und Seite mit Korrektur der Spiegelung zeichnen
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.99 sehen:
@implementation PDFView {
CGPDFDocumentRef document;
}
...
@end
Listing 6.99 Implementierungsblock mit Attributdeklaration
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.
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;
}
682
6.4
Scrollviews und gekachelte Layer
- (void)setDocument:(CGPDFDocumentRef)inDocument {
if(inDocument != document) {
CGPDFDocumentRelease(document);
document = inDocument;
CGPDFDocumentRetain(document);
}
}
6
Listing 6.100 »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 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.
Da die PDF-Dokumente keine Objective-C-Klasse haben, implementiert der PDFView 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.101.
683
6
Models, Layer, Animationen
+ (id)layerClass {
return [CATiledLayer class];
}
- (void)awakeFromNib {
[super awakeFromNib];
CATiledLayer *theLayer = (CATiledLayer *)self.layer;
theLayer.levelsOfDetail = 4;
theLayer.levelsOfDetailBias = 4;
}
- (void)layoutSubviews {
[super layoutSubviews];
CATiledLayer *theLayer = (CATiledLayer *)self.layer;
CGSize theSize = self.bounds.size;
theSize.height /= 4 * self.countOfPages;
theLayer.tileSize = theSize;
}
Listing 6.101 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.
- (void)viewDidLoad {
[super viewDidLoad];
NSURL *theURL = [[NSBundle mainBundle]
URLForResource:@"lorem-ipsum" withExtension:@"pdf"];
CGPDFDocumentRef theDocument = CGPDFDocumentCreateWithURL(
(__bridge CFURLRef) theURL);
self.pdfView.document = theDocument;
CGPDFDocumentRelease(theDocument);
Listing 6.102 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, mul-
684
6.5
Über diese Brücke musst du gehen
tiplizieren 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;
theFrame.size.height =
CGRectGetHeight(self.scrollView.frame) *
self.pdfView.countOfPages;
self.scrollView.contentSize = theFrame.size;
self.pdfView.frame = theFrame;
6
}
Listing 6.103 Anpassung der View-Höhe an das Dokument
6.5
Ü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, 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.4
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;
Listing 6.104 Toll-free Bridge
6.5.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
4 Zumindest nicht, wenn Sie Ihre App durch den Review-Prozess in den App Store bekommen
möchten.
685
6
Models, Layer, Animationen
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.
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.
CFStringRef *theReference = CFStringCreateCopy(...);
NSString *theString = (__bridge NSString *)theReference;
...
CFRelease(theReference);
Listing 6.105 Cast ohne Übertragung der Eigentümerschaft
Listing 6.105 ü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.106 überträgt der ARCCompiler 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.106 Umgekehrter Cast ohne Übertragung der Eigentümerschaft
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:
686
6.5
Über diese Brücke musst du gehen
CFStringRef CreateStringWithInteger(NSInteger inValue) {
NSString *theValue =
[[NSString alloc] initWithFormat:@"%d", inValue];
return (__bridge_retained CFStringRef)theValue;
}
Listing 6.107 Ü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.107 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.5.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.
Dass der Cast dennoch funktioniert, liegt daran, dass auch viele Datentypen in den CFrameworks sich in einer Typhierarchie mit einer gemeinsamen Menge von Funktionen befinden. Diese gemeinsamen Funktionen bilden dabei genau die Methoden
ab, die eine Objective-C-Klasse 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
687
6
6
Models, Layer, Animationen
[(id)theImage retain]. Diese transparente Abbildung der C-Datentypen auf Objec-
tive-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.6
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.
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.42).
688
6.6
Was Sie schon immer über Instruments wissen wollten, aber nie zu fragen wagten
Abbildung 6.42 Starten von Instruments
6
Nach dem Start öffnet Instruments den in Abbildung 6.43 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 Mess-Szenarien 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.
Abbildung 6.43 Auswahldialog in Instruments
689
6
Models, Layer, Animationen
Operation am lebenden Programm
Damit Sie einen anschaulichen Eindruck von Instruments bekommen, finden Sie auf
der beiliegenden DVD und im Git-Repository zum Buch das Beispielprojekt Instruments. Den Quellcode finden Sie auf der DVD unter Code/Apps/iOS6/Instruments
oder im Github-Repository zum Buch im Unterverzeichnis https://github.com/
Cocoaneheads/iPhone/tree/Auflage_3/Apps/iOS6/Instruments.
Die App stellt verschiedene Analysesituationen bereit und enthält deshalb absichtlich
Schwachstellen für die nachfolgend beschriebenen Mess-Szenarien, 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.6.1
Spiel mir das Lied vom Leak
Eine der wichtigste Aufgabe 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 in Kapitel 2, »Die Reise nach iOS«
Die Vorlage Leaks mit dem gleichnamigen Messinstrument kann Ihnen helfen, auch
solche Speicherverwaltungsfehler zu finden.
Öffnen Sie das Instruments-Projekt in Xcode, und starten Sie Instruments wie oben
beschrieben. Im Auswahldialog (siehe Abbildung 6.43) 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.44 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.
690
6.6
Was Sie schon immer über Instruments wissen wollten, aber nie zu fragen wagten
6
Abbildung 6.44 Speicheranalyse mit Instruments
Nach dem Start erscheint neben Allocations ein blaues »Gebirge«, das den verbrauchten Speicher der 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
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.45). Der Balken kennzeichnet das Auftreten eines Speicherlecks.
Abbildung 6.45 Grafische Anzeige eines Speicherlecks in Instruments
Oben links im Fenster befindet sich der Button Stop. 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 wie in Abbildung 6.45 zeigt das Pro-
691
6
Models, Layer, Animationen
gramm im unteren Fensterbereich Details zu den gefundenen Leaks an. In der linken
Spalte können Sie die Einstellungen des Instruments anpassen, während der Bereich
auf der rechten Seite die gefundenen Speicherlecks auflistet (siehe Abbildung 6.46).
Abbildung 6.46 Details des Leaks-Instruments
Einstellungssache
Leaks sammelt die Speicherlecks in regelmäßigen Intervallen. Die Intervall-Lä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.46) anklicken, gelangen Sie zu den Details des Lecks. Hier sehen Sie
unter anderem den Referenzzähler und die Erzeugungszeit relativ zum Programmstart. Durch Drücken des Reiters Leaks by Backtrace gelangen Sie wieder zurück
zur Übersicht mit allen Speicherlecks.
Sie können sich über (ª)+(cmd)+(E) oder den Menüpunkt View • Extended Detail
den Aufrufstapel anzeigen lassen (siehe Abbildung 6.47), 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.
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 User-Symbol, wie beispielsweise bei der ausgewählten Zeile in der Abbildung. Wenn Sie einen Doppelklick auf Zeile im Stapel ausführen, die in Abbildung
6.47 hervorgehoben ist, zeigt Instruments Ihnen die folgende Zeile an:
692
6.6
Was Sie schon immer über Instruments wissen wollten, aber nie zu fragen wagten
6
Abbildung 6.47 Anzeige eines einfachen Lecks mit Aufrufstapel
Abbildung 6.48 Hier erzeugt die App ein Speicherleck.
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 auch der Analyzer entdeckt 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.46) 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
693
6
Models, Layer, Animationen
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.49).
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 haben. 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.49.
Abbildung 6.49 Detailanzeige für ein komplexeres Leck
In der Spalte Responsible Caller sehen Sie die Methode, die die Operation aufruft.
In der ausgewählten Zeile steht dort beispielsweise –[InstrumentsViewController
makeAttributeLeak]. 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.108).
5
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.5
5 Es gibt durchaus einige bekannte Speicherlecks in Cocoa Touch; die Wahrscheinlichkeit, dass Ihr
Code den Fehler verursacht, ist jedoch wesentlich höher.
694
6.6
Was Sie schon immer über Instruments wissen wollten, aber nie zu fragen wagten
- (IBAction)makeAttributeLeak {
_attributeLeak = [[InstrumentsDemoObject object] retain];
}
Listing 6.108 Speicherleck bei Attributzuweisung
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.109 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 die konsequente Verwendung von Accessoren vermeiden.
Es können auch mehrere Objekte ein Speicherleck erzeugen. Wenn Sie den Button
Retain 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, markiert Instruments auch drei Zeilen anstatt einer (siehe
Abbildung 6.50).
Abbildung 6.50 Mehrere Verursacher eines Speicherlecks
Das Leck entsteht hier durch zwei Arrays, die sich gegenseitig enthalten, also einen
Retain-Zyklus 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.50 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änderli-
695
6
6
Models, Layer, Animationen
ches Objekt jedoch nahezu beliebig viele Objekte aufnehmen kann, muss es seinen
Speicher bei Bedarf vergrößern können, und das ist anscheinend hier geschehen.
6.6.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
2
3
4
Zombie
2
1
2
3
4
Zombie
3
5
6
7
8
3
4
neues Objekt
Abbildung 6.51 Ein Zombie im Hauptspeicher
Abbildung 6.51 veranschaulicht 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 her-
696
6.6
Was Sie schon immer über Instruments wissen wollten, aber nie zu fragen wagten
vorrufen. 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.
6
Abbildung 6.52 Instruments hat einen Zombie entdeckt.
Nach dem Start zeigt Instruments Ihnen den Speicherverbrauch der App an. Wenn
Sie zweimal auf den Button Object klicken, hält die Ausführung an. Instruments
zeigt einen Dialog wie in Abbildung 6.52. 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.53). Auch hier können Sie wieder die unproblematischen Ereignisse über den Button Unpaired ausblenden.
Abbildung 6.53 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 diese Zeile auch nicht direkt für den Fehler
verantwortlich sein. Der Verursacher ist entweder das Autorelease der zweiten oder
das Release der vierten Zeile.
697
6
Models, Layer, Animationen
Auch hier können Sie sich über den Aufruf von View • Extended Detail oder (ª)
+(cmd)+(E) jeweils den Aufrufstapel zu den Anweisungen auf der rechten Seite des
Fensters anzeigen lassen. Der Stapel stellt alle Systemsymbole in grauer Schrift mit
einem farbigen Icon dar. Dagegen stellt er die Symbole aus Ihrem Programmcode in
schwarzer Schrift mit einem schwarzen Icon dar. Abbildung 6.54 zeigt einen Ausschnitt eines Aufrufstapels. Mit einem Doppelklick auf diese Symbole springen Sie an
die entsprechende Stelle des Quelltextes.
Abbildung 6.54 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.110 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 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 extrem
selten auftreten.
698
6.6
6.6.3
Was Sie schon immer über Instruments wissen wollten, aber nie zu fragen wagten
Time Bandits
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.
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.
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 Funktionsund 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 linken Seite in der Rubrik Call Tree die Option Separate by Thread aus- 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.55 aussehen.
Abbildung 6.55 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 90 %.
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
699
6
6
Models, Layer, Animationen
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.56). 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.56 Zeitverbrauch der Anweisungen in einer Methode
Durch Anklicken des grauen Kreises mit dem »i« 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.
Anscheinend verbraucht die Methode sum ihre Rechenzeit fast komplett im Schleifenrumpf für den Aufruf der Methode successorWithIndex: (siehe Abbildung 6.57).
Instruments hebt diese Zeile freundlicherweise rot hervor und unterstreicht den
Methodennamen.
Abbildung 6.57 Dem Zeitverbrauch auf der Spur ...
Wenn Sie den Methodennamen anklicken, gelangen Sie zum Quelltext dieser
Methode. Auch hier zeigt Instruments Ihnen wieder den relativen Zeitverbrauch der
700
6.6
Was Sie schon immer über Instruments wissen wollten, aber nie zu fragen wagten
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.
6
Abbildung 6.58 ... 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 8-mal
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) {
theSum += theItem.counter;
theItem = theItem.successor;
}
return theItem;
}
Listing 6.111 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.
701
6
Models, Layer, Animationen
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.6.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.59).
Abbildung 6.59 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.
702