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