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