The
Pragmatic Programmers
iPad-
Programmierung Der schnelle Einstieg für iPhoneEntwickler
Deutsche Übersetzung von
O’REILLY
Daniel H. Steinberg & Eric T. Freeman Übersetzt von Lars Schulten
iPad-Programmierung Der schnelle Einstieg für iPhone-Entwickler
iPad-Programmierung Der schnelle Einstieg für iPhone-Entwickler
Daniel H. Steinberg Eric T. Freeman Deutsche Übersetzung von Lars Schulten
Beijing · Cambridge · Farnham · Köln · Sebastopol · Tokyo
Die Informationen in diesem Buch wurden mit größter Sorgfalt erarbeitet. Dennoch können Fehler nicht vollständig ausgeschlossen werden. Verlag, Autoren und Übersetzer übernehmen keine juristische Verantwortung oder irgendeine Haftung für eventuell verbliebene Fehler und deren Folgen. Alle Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt und sind möglicherweise eingetragene Warenzeichen. Der Verlag richtet sich im Wesentlichen nach den Schreibweisen der Hersteller. Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen. Kommentare und Fragen können Sie gerne an uns richten: O’Reilly Verlag Balthasarstr. 81 50670 Köln E-Mail:
[email protected] Copyright der deutschen Ausgabe: © 2011 by O’Reilly Verlag GmbH & Co. KG 1. Auflage 2011 Die Originalausgabe erschien 2010 unter dem Titel iPad Programming bei Pragmatic Bookshelf, Inc. Bibliografische Information der Deutschen Nationalbibliothek Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar.
Übersetzung und deutsche Bearbeitung: Lars Schulten, Köln Lektorat: Inken Kiupel, Köln Korrektorat: Sibylle Feldmann, Düsseldorf DTP: Andreas Franke, SatzWERK, Siegen; www.satz-werk.com Produktion: Andrea Miß, Köln Belichtung, Druck und buchbinderische Verarbeitung: Druckerei Kösel, Krugzell; www.koeselbuch.de ISBN 978-3-89721-578-8 Dieses Buch ist auf 100% chlorfrei gebleichtem Papier gedruckt.
Inhaltsverzeichnis 1
Vom iPhone zum iPad 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 1.10 1.11
2
1
Das iPad und der Laptop . . . . . . . . . . . . . . . . . . . . . . Das iPad und der iPod touch . . . . . . . . . . . . . . . . . . . Erste Schritte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ein Modell-Objekt ergänzen . . . . . . . . . . . . . . . . . . . . Das C in MVC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Detail-View und sein Controller . . . . . . . . . . . . . . Die Table-Delegate-Methode implementieren . . . . . . . Kompatibilitätsmodus . . . . . . . . . . . . . . . . . . . . . . . . Umwandlung in eine Universal-App . . . . . . . . . . . . . . Ein paar Striche ergänzen . . . . . . . . . . . . . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . .
Split-Views 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 2.10 2.11
Den Split-View-Controller im IB einführen . . . . . Mit dem Split-View-Controller interagieren . . . . . Zwischen den View-Controllern kommunizieren . Unterschiedliche Geräte mit Unterklassen berücksichtigen . . . . . . . . . . . . . . . . . . . . . . . . . Die App-Delegates trennen . . . . . . . . . . . . . . . . . Dem Detail-View eine Werkzeugleiste hinzufügen Das Split-View-Delegate . . . . . . . . . . . . . . . . . . . Ein Popover einfügen . . . . . . . . . . . . . . . . . . . . . Popover und Button entfernen . . . . . . . . . . . . . . Eine iPad-spezifische, Split-View-basierte App erstellen . . . . . . . . . . . . . . . . . . . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . .
2 3 6 8 10 11 13 14 15 18 21 25
.... .... ....
27 29 32
. . . . . .
... ... ... ... ... ...
33 36 39 40 42 44
.... ....
45 47
VI X Inhaltsverzeichnis 3
Gesten nutzen 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9
4
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
Auf Berührung reagieren . . . . . . . . . . . . . . . . Den Controller für die Farben erstellen . . . . . . Modale Views einblenden . . . . . . . . . . . . . . . . Den Controller bereinigen . . . . . . . . . . . . . . . . Ein Popover anzeigen. . . . . . . . . . . . . . . . . . . . Ein erneuter Blick auf Split-View und Popover Popover für Buttons . . . . . . . . . . . . . . . . . . . . Die Ausrichtung ändern . . . . . . . . . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . . . .
Einfache Texteingabe. . . . . . . . . . . . . Angepasste Tastaturen erstellen . . . . Auf die Tasten reagieren . . . . . . . . . . Einen Accessory-View hinzufügen . . . Tastaturbenachrichtigungen nutzen . Den Text-View animieren. . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. 82 . 84 . 85 . 88 . 89 . 92 . 93 . 97 . 100 101
..... ..... ..... ..... ..... ..... .....
..... ..... ..... ..... ..... ..... .....
... ... ... ... ... ... ...
Zeichnen 6.1 6.2 6.3 6.4 6.5 6.6 6.7
Zeichnen mit Core Graphics . . . . . . . Die Cocoa-APIs nutzen . . . . . . . . . . . Kreise und Rechtecke zeichnen . . . . . Unregelmäßige Pfade . . . . . . . . . . . . Bézierkurven nutzen . . . . . . . . . . . . . Unsere Zeichnung als PDF speichern. Zusammenfassung . . . . . . . . . . . . . .
50 51 55 58 61 65 75 77 80 81
Angepasste Tastaturen 5.1 5.2 5.3 5.4 5.5 5.6 5.7
6
iPad Virtual Bubble Wrap . . . . . . . . . . . . . . . Einfache Tap-Gesten nutzen . . . . . . . . . . . . . Multi-Touch-Events und die View-Hierarchie . UIGestureRecognizer und die Swipe-Geste . . Diskrete und kontinuierliche Gesten . . . . . . . Eigene Gesten erstellen . . . . . . . . . . . . . . . . . Was hat denn da geknallt?. . . . . . . . . . . . . . . Recognizer-Konflikt . . . . . . . . . . . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . . .
Popover und modale Dialoge 4.1 4.2 4.3 4.4 4.5 4.6 4.7 4.8 4.9
5
49
102 104 105 107 109 110 113 115
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
116 120 123 125 127 129 132
Inhaltsverzeichnis W VII 7
Der Movie Player 7.1 7.2 7.3 7.4 7.5 7.6
8
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
Der Movie Player, Phase 2 8.1 8.2 8.3 8.4 8.5 8.6 8.7 8.8
9
Einen View für Videos einrichten Einblick in den Player . . . . . . . . Benachrichtigt werden . . . . . . . . Eine Wiedergabeliste hinzufügen Thumbnails erstellen . . . . . . . . . Vorschau . . . . . . . . . . . . . . . . . .
133
159
Video-Shoutouts . . . . . . . . . . . . . . . . . . . . . . Eigene Wiedergabesteuerung . . . . . . . . . . . . . Die Steuerelemente implementieren . . . . . . . . Die Wiedergabezeit verwalten . . . . . . . . . . . . . Eine Videonavigation implementieren . . . . . . . Die Wiedergabesteuerung dynamisch machen. Der Vollbildmodus . . . . . . . . . . . . . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
Apples HTTP-Live-Streaming 9.1 9.2 9.3 9.4 9.5
Progressives Video vs. Streamed-Video . . . . Apples HTTP-basiertes Streaming-Protokoll Einen Streaming-Player erstellen . . . . . . . . Auf die Netzwerkumgebung reagieren . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . .
.. .. .. .. ..
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
Ein externes Anzeigegerät erkennen . . . . . . . . . . . . Einfache Ausgabe auf ein externes Display . . . . . . . Videoinhalte auf den externen Bildschirm schicken Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . .
11.7 11.8
Das Monty Hall-Problem . . . . . . . . . . . . . . Den Server starten und bekannt machen . Den Client starten und verbinden . . . . . . . Die Spiellogik ergänzen . . . . . . . . . . . . . . . Daten an ein anderes Gerät senden . . . . . Von einem anderen Gerät gesendete Daten empfangen . . . . . . . . . . . . . . . . . . . Aufräumen . . . . . . . . . . . . . . . . . . . . . . . . Peers bekannt machen . . . . . . . . . . . . . . .
185 186 190 191 193 195
. . . .
. . . .
11 Geräte verbinden 11.1 11.2 11.3 11.4 11.5 11.6
159 163 169 170 173 178 181 183 185
10 Externe Anzeigegeräte nutzen 10.1 10.2 10.3 10.4
133 139 142 145 150 157
196 200 204 207 209
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
210 211 213 215 217
. . . . . . . . . 218 . . . . . . . . . 220 . . . . . . . . . 220
VIII X Inhaltsverzeichnis 11.9 Peers verbinden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222 11.10 Chatten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223 11.11 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . 225 12 Mit Dokumenten arbeiten 12.1 12.2 12.3 12.4 12.5 12.6 12.7
Dokumente mit iTunes übertragen Dokumente dauerhaft speichern . . Dateitypen registrieren . . . . . . . . . Eine Datei beim Start öffnen . . . . . Dateien öffnen . . . . . . . . . . . . . . . Dateivorschau . . . . . . . . . . . . . . . Zusammenfassung . . . . . . . . . . . .
227 . . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
13 Die große Zusammenfassung 13.1 13.2
Denken Sie immer zuerst an den Benutzer . . . . . Behandeln Sie Landscape- und Portrait-Modus gleichrangig . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.3 Die Hierarchie glätten . . . . . . . . . . . . . . . . . . . . . 13.4 Erstellen Sie detailreiche, realistische Views . . . . 13.5 Gesten sind mächtig . . . . . . . . . . . . . . . . . . . . . . 13.6 Das iPad will kommunizieren . . . . . . . . . . . . . . . 13.7 Dokumente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.8 Video ist wichtig . . . . . . . . . . . . . . . . . . . . . . . . . 13.9 Externe Anzeigegeräte verlangen eine angepasste Implementierung . . . . . . . . . . . . . . . . 13.10 Verbessern Sie die Leistungen Ihrer App mit Video-Streaming . . . . . . . . . . . . . . . . . . . . . . 13.11 Danksagungen . . . . . . . . . . . . . . . . . . . . . . . . . .
228 229 231 233 234 237 238 239
. . . . 240 . . . . . . .
.. .. .. .. .. .. ..
. . . . . . .
240 241 242 242 242 243 243
. . . . 244 . . . . 244 . . . . 244
Literaturverzeichnis
247
Index
249
Kapitel 1
Vom iPhone zum iPad Noch bevor Sie auch nur eine einzige Zeile Code für Ihre iPad-App schreiben, müssen Sie das Gerät in den Händen halten. Sie müssen spüren, wie das iPad in den Hintergrund tritt, sobald Sie sich einer App zuwenden. Und auch die App sollte nicht mehr bewusst wahrnehmbar sein, sobald Sie sich in sie vertiefen. Das ist die Art von App, die uns vorschwebt. Wir wollen Sie nicht dazu bringen, Apps zu schreiben, die einfach auf dem iPad laufen. Wir möchten, dass Sie Apps schreiben, die perfekt auf die Plattform zugeschnitten sind. Die Nutzer Ihrer App werden ihren Freunden davon vorschwärmen und die Anwendung begeistert weiterempfehlen. Wie wird das möglich? Ist das iPad nicht nur ein übergroßer iPod touch, auf dem das uns bekannte und geliebte iOS läuft?1 Haben wir uns etwa von dem Marketing-Hype blenden lassen, den Steve Jobs so geschickt erzeugen kann? Nein. Dieses hochauflösende Gerät mit seinem großen Bildschirm, das Sie in den Händen halten, sich aber nicht in die Tasche stecken können – ein Gerät, das Sie mit Multi-Touch und Gesten steuern – ist etwas völlig anderes. 1 Auf der WWDC 2010 wurde das zuvor unter dem Namen iPhone OS bekannte Betriebssystem in iOS umgetauft, aber selbst bei iOS 4.3 ist diese Namensänderung für das SDK nicht übernommen worden.
2 X Kapitel 1: Vom iPhone zum iPad Über das gesamte Buch hinweg werden wir Ihnen zeigen, wie Sie eine hervorragende User Experience für dieses Gerät erzeugen. Wir werden die Gemeinsamkeiten und Unterschiede zwischen der Entwicklung für das iPad und der Ihnen vertrauten Entwicklungsarbeit für das iPhone aufzeigen. Wir verwenden jedoch kein einheitliches Beispiel, das alles auf einen Blick demonstrieren kann. Stattdessen beleuchten wir verschiedene, voneinander unabhängige Themen, die Sie bei Bedarf in Ihre eigenen Apps integrieren können. Wir werden mit der Ihnen vertrauten Welt beginnen. Wir starten in diesem Kapitel damit, eine iPhone-App zu erstellen, und wandeln sie dann in eine App um, die auf dem iPad läuft. Lassen Sie uns aber zunächst herausfinden, welchen Platz das iPad im Leben vieler Anwender hat – irgendwo zwischen einem Laptop und einem iPhone.
1.1
Das iPad und der Laptop Sobald Sie einmal mit einem iPad Ihre E-Mails gelesen haben oder im Web gesurft sind, wird Ihnen Ihr Laptop sperriger und weniger portabel erscheinen als zuvor. Ganz plötzlich wird Ihnen bewusst werden, wie viel Platz zur Unterbringung von Tastatur, Trackpad, Festplatte und Akku erforderlich ist. Auf der anderen Seite ist Ihr iPad aber auch einigen Beschränkungen unterworfen. Es ist nicht als eigenständiges Gerät gedacht. Es muss gelegentlich mit einem Laptop oder einem Desktop-Rechner synchronisiert werden, bietet nicht die Rechenkraft, den Arbeitsspeicher und den Speicherplatz eines Laptops und ist für einen einzigen Benutzer konzipiert, dessen Aufmerksamkeit jeweils nur auf eine einzige Anwendung gerichtet ist. Seit iOS 4.2 bietet das iPad zwar Multitasking-Unterstützung, aber die Schnittstelle basiert weiterhin auf Anwendungen, die den gesamten Bildschirm einnehmen. Das iPad ist nicht als die digitale Schaltzentrale konzipiert, zu der ein Desktop-Rechner oder Laptop werden kann. Aktuell ist es nur ein Knoten in diesem Schaltnetz, nicht der Mittelpunkt Ihres digitalen Universums. Der wichtigste Aspekt aber ist, dass es einen wesentlichen Unterschied zwischen der Touch-basierten Bildschirminteraktion und der Steuerung mit einer Maus oder einem Trackpad gibt. Auf dem iPad nutzen Sie Ihre Finger, um in Flight Control Ihre Flugzeuge zu landen, in Brushes wunderbare Bilder zu zeichnen oder E-Mails zum Löschen zu markie-
Das iPad und der iPod touch W 3 ren. Wenn Sie ein Objekt markieren oder bewegen wollen, setzen Sie Ihren Finger darauf, bewegen ihn, und das Objekt wird ihm brav folgen. Wollen Sie die Größe eines Objekts ändern, setzen Sie zwei Finger auf den Bildschirm und führen sie zusammen oder spreizen sie auseinander. Sie nutzen Ihre Finger, um die Größe oder die Position eines Bilds zu ändern oder ein Bild zu beschneiden. Sie zoomen in eine Webseite hinein, damit Sie sie besser lesen und Ihre Finger die Textfelder leichter anwählen können, in die Sie Text eingeben. Diese Gesten sind so selbstverständlich geworden, dass es einem fremd vorkommt, wenn man eine Maus nutzen muss, um ein Objekt auszuwählen, und Anfasser anklicken muss, um seine Größe oder Position zu ändern. Bereits der Griff zu Maus oder Trackpad wirft uns inzwischen aus der Bahn. Der letzte wichtige Unterschied zwischen einem iPad und einem Laptop ist, dass Sie das iPad einfach um 90 Grad drehen können und Ihnen dann eine ganz andere Ansicht der App präsentiert wird. Für den iPadEntwickler heißt das, dass er sich speziell darüber Gedanken machen muss, wie seine App im Landscape- oder Portrait-Modus am besten darzustellen ist.
1.2
Das iPad und der iPod touch Wesentliche Elemente der Ihnen vertrauten und von Ihnen geschätzten grafischen Oberfläche von Mac OS X (und später auch von Windows) wurden ursprünglich bei Xerox PARC von einem Team um Alan Kay entwickelt. Immer wieder hat Kay bemängelt, dass sich die Art, einen Computer zu bedienen, zu wenig verändert hat und immer noch starke Ähnlichkeiten aufweist zu den Interaktionsmöglichkeiten, an denen er schon vor Jahrzehnten mit seinem Team gearbeitet hat. Alan Kay erzählte Janko Roettgers von Gigaom folgende Geschichte zu seiner Reaktion bei der Veröffentlichung des iPhones im Jahr 2007: „Als der Mac auf den Markt kam, fragte mich der Newsweek, was ich von dem Gerät halte. Ich antwortete: ‚Ich denke, es ist der erste Personal Computer, der einen genaueren, kritischen Blick verdient.‘ Gegen Ende der iPhone-Präsentation kam Steve zu mir und fragte: ‚Ist das iPhone einen genaueren, kritischen Blick wert?‘ Und ich antwortete: ‚Wenn ihr den Bildschirm 5 x 8 Zoll groß macht, werdet ihr damit die Welt erobern.‘ “2
2
http://gigaom.com/2010/01/26/alan-kay-with-the-tablet-apple-will-rule-the-world/
4 X Kapitel 1: Vom iPhone zum iPad Und ist damit das iPad nicht schön umrissen? Ist das iPad nicht bloß ein großer iPod touch? Es stimmt sicherlich, dass auf dem iPad iOS läuft, also genau das Betriebssystem, das auch auf iPhone und iPod touch läuft. Und wenn Sie iPad-Apps schreiben, werden Sie die gleiche API nutzen, die Sie auch beim Schreiben von iPhone-Apps verwenden. Es stimmt sicherlich auch, dass das iPad erheblich größer ist als das iPhone. Das iPad ist ungefähr genauso groß wie die gedruckte Fassung dieses Buchs – etwa einen Zentimeter höher und ein wenig dünner, aber ungefähr genauso breit. Das iPhone ist erheblich kleiner. Hier ist ein Bild, das Ihnen das Größenverhältnis zwischen zwei iPhone-Bildschirmen und dem iPad-Bildschirm demonstrieren soll:
Das iPad ist also ein großer iPod touch – aber es ist nicht nur ein großer iPod touch. Dass es größer ist, macht es jedoch zu etwas ganz Speziellem! Die Größe ist ein ganz zentraler Punkt. Auf dem iPad können Sie Webseiten in voller Auflösung betrachten, Bilder mit einem Fingerschnippen stapeln und Formulare mit einer Tastatur ausfüllen, auf der Sie sämtliche zehn Finger platzieren können.
Das iPad und der iPod touch W 5 Diesen entscheidenden Punkt werden Sie übersehen, wenn Sie das iPad ausschließlich aus der Perspektive des Entwicklers betrachten. Die APIs sind denen für das iPhone so ähnlich, dass Apps für das eine Gerät problemlos auf dem anderen laufen können. Wenn Sie also damit beginnen, eine App für das iPad zu entwickeln, sollten Sie sich die Zeit nehmen, darüber nachzudenken, was an diesem Gerät anders und besonders ist.
Was Sie für dieses Buch benötigen Sie müssen sich kostenlos unter http://developer.apple.com/programs/register als iPhone-Entwickler registrieren. Eine zusätzliche Gebühr (aktuell 79 Euro) wird fällig, wenn Sie Ihre Apps auf Ihrem iPad oder iPhone installieren oder über den iTunes App Store vertreiben wollen. Einige der Beispiele in diesem Buch, beispielsweise das Sharing und der Einsatz externer Bildschirme, müssen auf dem Gerät ausgeführt werden. Wenn Sie sich registriert haben, können Sie die neuesten Werkzeuge und SDKs herunterladen. Aktuell benötigen Sie für die iPadEntwicklung einen Intel-Mac, auf dem Mac OS X 10.6.6 läuft. Die Beispiele für dieses Buch wurden mit Xcode 3.2.6 und iPhone SDK 4.3 erstellt. Wir setzen voraus, dass Sie den modernen Simulator nutzen, der das erste Mal mit Xcode 3.2.3 ausgeliefert wurde. Sie können alle Projekte für dieses Buch von der Homepage des Buchs unter http://pragprog.com/titles/sfipad/ herunterladen. Wir haben die Projekte in Phasen aufgeteilt, damit Sie die Beispiele nacharbeiten können oder an einem von Ihnen gewählten Punkt Ihre Arbeit mit unserer vergleichen können. Die von Apple gelieferten Projektvorlagen unterliegen ständiger Veränderung. Sie werden gelegentlich also auf kleine Unterschiede stoßen.
Wahrscheinlich schätzen Sie iPhone-Apps, die alle Fähigkeiten des iPhones auf natürliche Weise nutzen,3 Apps, die den Beschleunigungssensor, den Kompass, das GPS, die Kamera und das Telefon selbst berücksichtigen, wenn es sinnvoll ist. Im restlichen Buch werden wir 3 Da es lästig ist, jedes Mal „iPhone und iPod touch“ zu sagen, werden wir häufig den einen oder den anderen Begriff nutzen, um auf die gemeinsame Plattform zu verweisen.
6 X Kapitel 1: Vom iPhone zum iPad Ihnen zeigen, wie man das iPad programmiert. Was Sie programmieren wollen, müssen Sie selbst herausfinden.
1.3
Erste Schritte In diesem Buch setzen wir voraus, dass Sie wissen, wie man iPhoneApps entwickelt. Vielleicht haben Sie zum Spaß oder aus beruflichen Gründen die eine oder andere iPhone-App entwickelt. Sie sind bereits sehr vertraut mit Cocoa, Objective-C und den iPhone-APIs. Sie haben sich in die neuesten Versionen von Xcode und Interface Builder eingearbeitet. Sie wissen, wie man mit View-Controllern und Delegates arbeitet, und beherrschen die Speicherverwaltung mit Referenzzählern. An diesem Punkt setzen wir an. Wir werden eine iPhone-App erstellen und dabei einige der Schlüsselkonzepte wiederholen, die Sie für dieses Buch beherrschen müssen. Diese Techniken nutzen die iPhone- und die iPad-Entwicklung gemeinsam. Das werden Sie zum Abschluss dieses Kapitels sehen, wenn wir dieses Projekt, das nur auf dem iPhone läuft, in eins umwandeln, das auf iPhone und iPad gleichermaßen läuft. Am Ende dieses Kapitels werden Sie wissen, ob Ihre Kenntnisse für dieses Buch ausreichen. Ihnen sollte alles vertraut sein, und der kurze Überblick muss ausreichen, um Sie fit zu machen für den Rest des Buchs. Sind Sie jedoch verwirrt oder haben den Eindruck, wir schritten zu schnell voran, sollten Sie mit einem Buch zur Cocoa- oder iPhoneEntwicklung beginnen.4 Wenn Sie hingegen meinen, dass wir zu langsam vorgehen und Sie sich gleich dem iPad-Material zuwenden wollen, können Sie mit dem Projekt aus dem FromTo/DailyShoot1-Verzeichnis beginnen und direkt zum Abschnitt 1.8, Kompatibilitätsmodus, springen. Wir werden eine einfache, navigationsbasierte Anwendung auf Grundlage der The Daily Shoot-Website (http://dailyshoot.com) von Mike Clark und James Duncan Davidson erstellen. Diese Site schlägt Fotografen seit November 2009 täglich eine Aufgabe vor, die sie fotografisch umsetzen sollen. Bei einer App dieser Art navigieren Benutzer durch einige Ebenen von Tabellenansichten, bis sie ein einzelnes Element auswählen und zu seiner Detailansicht gelangen. 4 Sie können beginnen mit Cocoa-Programmierung: Der schnelle Einstieg für Entwickler [Ste09] und/oder mit Entwickeln mit dem iPhone SDK [DA09].
Erste Schritte
Verschwindende ivars Wenn Sie bereits seit einer Weile für das iPhone entwickeln, haben Sie sich bei der Deklaration von Eigenschaften wahrscheinlich an einen bestimmten Rhythmus gewöhnt. Sie deklarieren einen ivar und die entsprechende Eigenschaft in der Header-Datei, synthetisieren die Eigenschaft und geben sie in der Implementierungsdatei frei. Seit es den neuen mit Xcode 3.2.3 veröffentlichten Simulator gibt, müssen Sie den ivar nicht mehr deklarieren. Die Instanzvariablen werden zur Laufzeit im Simulator synthetisiert, als wären sie die ganze Zeit auf dem Gerät gewesen. Schon bald werden sie die Eigenschaft nicht mehr explizit synthetisieren müssen, wenn „synthesize by default“ als Compiler-Option verfügbar ist.
Unsere App wird mit einer einfachen Tabelle beginnen, die mit den Nummern der ersten 125 fotografischen Aufträge gefüllt ist. Wenn ein Benutzer eine Nummer wählt, werden wir die entsprechende Zielseite für den Auftrag anzeigen. Das ist ein recht einfaches Beispiel, aber es ermöglicht uns, die gesamte Maschinerie einzusetzen, die wir für eine navigationsbasierte Anwendung benötigen, und legt den Grundstein für die Split-Viewbasierte App im nächsten Kapitel.
W
7
8 X Kapitel 1: Vom iPhone zum iPad Starten Sie Xcode und erstellen Sie eine navigationsbasierte App, die zur Speicherung nicht Core Data nutzt. Nennen Sie das Projekt DailyShoot. Übrigens: Vielleicht ist Ihnen aufgefallen, dass es zwischen den verfügbaren Projektvorlagen entscheidende Unterschiede gibt. Die Split-Viewbasierte Projektvorlage ist neu und kann nur für iPad-basierte Projekte genutzt werden. Die Vorlagen für navigationsbasierte Apps und UtilityApps können nur für iPhone-Projekte verwendet werden. Die Windowbasierte Vorlage kann für ein iPhone-Projekt, ein iPad-Projekt oder ein Universalprojekt für beide Plattformen genutzt werden. Die verbleibenden Vorlagen stehen als Basis für iPhone- und iPad-Projekte zur Verfügung, aber Sie müssen in einer Liste auswählen, was Sie nutzen wollen. Wir werden nichts mit dem App-Delegate oder der Main-Window-Nib tun. Wäre es eine Produktions-App, würden wir uns die Zeit nehmen, um dem Namen der RootViewController -Klasse und der entsprechenden Nib etwas mehr Aussagekraft zu verleihen. Weil man bei diesen Änderungen leicht Fehler machen kann und weil die Änderungen nichts mit dem zu tun haben, was wir in diesem Kapitel machen wollen, werden wir die Namen lassen, wie sie sind.
1.4
Ein Modell-Objekt ergänzen Ein Table-View benötigt eine Datenquelle, die drei elementare Fragen beantworten kann: 앫
Wie viele Abschnitte hat die Tabelle?
앫
Wie viele Zeilen gibt es in diesen Abschnitten?
앫
Woraus bestehen die einzelnen Zeilen?
In dem für uns mithilfe der Vorlage erstellten Projekt ist der RootViewController die Datenquelle des Table-View. Wir werden unserem Projekt ein Modell-Objekt hinzufügen. Das entspricht der klassischen Model/View/Controller-Entkopplung (MVC). Der View redet mit dem Controller und der Controller mit dem Modell. tableView: numberOfRowsInSection:
UITableView
RootViewController
count:
Assignments
Ein Modell-Objekt ergänzen Der Table-View sendet die Nachricht tableView:numberOfRowsInSection: an den Controller. Der Controller seinerseits sendet die Nachricht count an das Assignments-Objekt, unser Modell-Objekt. Das Modell-Objekt antwortet dem Controller mit der Anzahl der Aufträge, die der Controller dann an den Table-View weiterleitet, damit er weiß, wie viele Zeilen angezeigt werden. Das implementieren Sie, indem Sie in Xcode eine neue Objective-CKlasse ergänzen, die eine Unterklasse von NSObject ist, und ihr den Namen Assignments geben. Ein Assignments-Objekt muss dem Controller mitteilen können, wie viele Aufträge es gibt und welche Auftragsnummer in eine bestimmte Zeile kommt. Es hält die Aufträge intern in einem NSArray namens assignmentArray fest. All das wird in die Header-Datei gepackt: FromTo/DailyShoot1/Classes/Assignments.h
#import
@interface Assignments : NSObject { NSArray *assignmentArray; } -(NSUInteger) count; -(NSNumber *) assignmentAtIndex:(NSUInteger) index; @end
Mehr verlangt unsere Implementierung nicht. Die Methoden count und assignmentAtIndex: leiten lediglich die Informationen aus dem Array assignment weiter. Die anderen Methoden erstellen das Array und geben es wieder frei. FromTo/DailyShoot1/Classes/Assignments.m
#import "Assignments.h" @implementation Assignments -(NSUInteger) count { return [assignmentArray count]; } -(NSNumber *) assignmentAtIndex:(NSUInteger) index { return [assignmentArray objectAtIndex:index]; } -(void) awakeFromNib { NSMutableArray *temp = [NSMutableArray array]; for (int i = 0;i < 125;i++){ [temp addObject:[NSNumber numberWithInt:125 -i]]; } assignmentArray = [[NSArray alloc] initWithArray:temp]; }
W
9
10 X Kapitel 1: Vom iPhone zum iPad -(void) dealloc { [assignmentArray release], assignmentArray = nil; [super dealloc]; } @end
Speichern Sie Ihre Arbeit und öffnen Sie im Interface Builder (IB) die RootViewController.xib. Wählen Sie den Classes-Reiter der Library und suchen Sie nach der gerade erstellten Klasse Assignments. Ziehen Sie sie ins Document-Fenster, um eine Instanz von Assignments zu erstellen. Speichern Sie anschließend. Jetzt ist das Modell-Objekt vollständig. Verbinden wir es mit dem Controller.
1.5
Das C in MVC Der RootViewController muss mit dem gerade erstellten Modell-Objekt kommunizieren. Beide werden in der gleichen Nib instantiiert, können also leicht verbunden werden. Fügen Sie in RootViewController.h eine Eigenschaft des Typs Assignments ein und deklarieren Sie sie als Bean-Outlet. FromTo/DailyShoot1/Classes/RootViewController.h
#import X @class Assignments; @interface RootViewController : UITableViewController { } X @property(nonatomic, retain) IBOutlet Assignments *assignments; @end
Speichern Sie die Header-Datei und gehen Sie in den IB, um das Outlet mit dem Assignments-Objekt in RootViewController.xib zu verbinden. Speichern Sie Ihre Arbeit im IB. Importieren Sie Assignments.h oben in RootViewController.m und synthetisieren Sie die Variable assignments. Setzen Sie in viewDidLoad den title des Table-View auf Aufträge und liefern Sie YES aus der Methode shouldAutorotateToInterfaceOrientation:. FromTo/DailyShoot1/Classes/RootViewController.m
#import "RootViewController.h" #import "Assignments.h" #import "AssignmentViewController.h" @implementation RootViewController @synthesize assignments; - (void)viewDidLoad { [super viewDidLoad];
Der Detail-View und sein Controller self.title = @"Aufträge"; } - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; }
Sie müssen nun die folgenden hervorgehobenen Änderungen an den Datenquellenmethoden vornehmen: FromTo/DailyShoot1/Classes/RootViewController.m
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { X return [self.assignments count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; } cell.textLabel.text = [[self.assignments assignmentAtIndex:indexPath.row] stringValue];
X X X
return cell; }
Sie brauchen also keine sonderlich große Menge Code hinzuzufügen. Klicken Sie auf „Build and Run“, sollte die App erfolgreich erstellt und gestartet werden. Im Table-View müssten Sie jetzt die Auftragsnummern in aufsteigender Folge sehen. Das ist bereits die halbe Miete.
1.6
Der Detail-View und sein Controller Wenn der Endanwender im Table-View eine Auftragsnummer wählt, werden wir einen neuen View anzeigen, der einen Web-View mit der Zielseite des Auftrags enthält. Wir müssen die Delegate-Methode TableView:didSelectRowAtIndexPath: so implementieren, dass sie einen View-Controller zur Steuerung dieses View erstellt, und dann müssen wir den View-Controller auf den vom Navigations-Controller verwalteten Stack schieben.
W
11
12 X Kapitel 1: Vom iPhone zum iPad tableView:didSelectRowAtIndexPath:
UITableView
Create VC Push page on stack
RootViewController
In diesem Abschnitt werden wir den View-Controller und die entsprechende Nib erstellen. Im nächsten Abschnitt werden wir die DelegateMethode implementieren. Erstellen Sie eine neue UIViewController -Unterklasse und wählen Sie die Option “With XIB for user interface”. Nennen Sie sie AssignmentViewController. Sie werden feststellen, dass sich das leichter erreichen lässt, wenn Sie die Nib-Datei in die Gruppe Resources verschieben und den Quellcode in der Gruppe Classes lassen. Der AssignmentViewController benötigt ein Outlet, damit er mit seinem WebView kommunizieren kann, und eine Eigenschaft, damit der RootViewController die Auftragsnummer übergeben kann, die der vom Benutzer gewählten Zeile entspricht. FromTo/DailyShoot1/Classes/AssignmentViewController.h
#import @interface AssignmentViewController : UIViewController { } @property (nonatomic, retain) IBOutlet UIWebView *webView; @property (nonatomic, retain) NSNumber *assignmentNumber; @end
Klicken Sie doppelt auf die AssignmentViewController -Nib-Datei, um sie mit dem IB zu öffnen. Nutzen Sie den Attributes-Inspektor, um die obere Leiste des View auf „Navigation Bar“ zu setzen. Fügen Sie in den View einen Web-View als Unterview ein und passen Sie seine Größe so an, dass er den gesamten verbleibenden Platz im View einnimmt. Wählen Sie im Attributes-Inspektor für den Web-View das Kästchen „Scale Pages To Fit“. Verbinden Sie das webView-Outlet des AssignmentViewController mit dem Web-View. Speichern Sie und beenden Sie anschließend den IB. Synthetisieren Sie zur Implementierung der Klasse AssignmentViewController die Variablen, laden Sie die erforderliche URL und setzen Sie den Titel für die Seite in der Methode viewDidLoad.
Die Table-Delegate-Methode implementieren FromTo/DailyShoot1/Classes/AssignmentViewController.m
@synthesize webView, assignmentNumber; - (void) loadSelectedPage { NSString *url = [NSString stringWithFormat:@"http://dailyshoot.com/assignments/%@", self.assignmentNumber]; [self.webView loadRequest: [NSURLRequest requestWithURL: [NSURL URLWithString:url]]]; } -(void)viewDidLoad { [super viewDidLoad]; self.title = [NSString stringWithFormat:@"Auftrag #%@", self.assignmentNumber]; [self loadSelectedPage]; }
Jetzt ist der Punkt erreicht, an dem Sie den View-Controller gemeinsam mit dem von ihm gesteuerten View auf den Stack schieben. Implementieren Sie außerdem die Methode shouldAutoRotateToInterfaceOrientation: so, dass sie YES ausgibt.
1.7
Die Table-Delegate-Methode implementieren Ein Table-View hat eine Datenquelle und ein Delegate. Die erforderlichen Datenquellenmethoden haben wir bereits im RootViewController implementiert. Das sind die Methoden, die dem Table-View sagen, was er anzeigen muss. Die Delegate-Methoden sind dafür verantwortlich, auf Benutzerinteraktionen mit der Tabelle zu reagieren. Namentlich die Delegate-Methode tableView:didSelectRowAtIndexPath: wird aufgerufen, wenn ein Benutzer einen Auftrag auswählt. In diesem navigationsbasierten Beispiel ist RootViewController das Delegate für den Table-View. Im nächsten Kapitel werden Sie sehen, dass das nicht immer der Fall sein muss. Der Inhalt der Methode tableView:didSelectRowAtIndexPath: ist zu großen Teilen bereits für Sie in RootViewController.m vorgegeben. Wir müssen nur den Namen der Controller-Klasse und der Nib ändern, die in AssignmentViewController geladen wird. Außerdem müssen wir den Wert der Eigenschaft assignmentNumber auf die in der vom Benutzer gewählten Zeile angezeigte Nummer setzen, bevor wir den View-Controller auf den Stack schieben.
W
13
14 X Kapitel 1: Vom iPhone zum iPad FromTo/DailyShoot1/Classes/RootViewController.m
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { AssignmentViewController *detailViewController = [[AssignmentViewController alloc] initWithNibName:@"AssignmentViewController" bundle:nil]; detailViewController.assignmentNumber = [self.assignments assignmentAtIndex:indexPath.row]; [self.navigationController pushViewController:detailViewController animated:YES]; [detailViewController release]; }
Vergessen Sie nicht, AssignmentViewController.h zu importieren. Damit sollte alles funktionieren, wenn Sie die App erstellen und ausführen. Jetzt ist unsere iPhone-App fertig. Das ist eine gute Gelegenheit für eine Bestandsaufnahme. Wir haben mit Nibs gearbeitet, Outlets gesetzt, ein Beispiel des MVC-Musters implementiert, Delegate-Methoden genutzt sowie geändert, was auf dem Bildschirm angezeigt wird, indem wir einen neuen View-Controller und eine Nib erstellten, die diesen als File’s Owner hat. Wenn Sie keine Probleme dabei hatten, diesem Beispiel zu folgen, ist Ihre Form absolut ausreichend für diese Einführung in die iPad-Programmierung. Jetzt aber ist es an der Zeit, dieses Projekt von einem iPhone-Projekt in ein Projekt umzuwandeln, das auf beiden Plattformen funktioniert.
1.8
Kompatibilitätsmodus Die von uns gerade erstellte App läuft auf dem iPad im Kompatibilitätsmodus. Es ist allerdings noch keine iPad-App, sondern nur eine iPhone-App, die wir auf dem iPad ausführen können. Wenn wir uns das im Simulator ansehen wollen, müssen wir nur eine Sache anpassen. Prüfen Sie, ob im Project-Menü „Set Active SDK“ auf „Simulator“ gesetzt ist und „Set Active Executable“ auf „Daily Shoot – iPad Simulator 4.3.“ Starten Sie die App, müsste der iPad-Simulator geöffnet werden.5
5 Viele der iPad-Screenshots werden im Landscape-Modus dargestellt oder für die Darstellung gedreht, um Platz zu sparen.
Umwandlung in eine Universal-App
Das ist schon recht nett. Apple bietet mit dem Kompatibilitätsmodus ein einfaches Verfahren, iPhone-Apps auf dem iPad auszuführen, ohne dass der Entwickler etwas ändern muss. Dieser Kompatibilitätsmodus ist eine Annehmlichkeit für Entwickler, aber keine, auf die Sie sich je verlassen sollten. Auf dem iPad kann eine iPhone-App in ihrer ursprünglichen Größe betrachtet werden. Und genau wie auf dem Gerät können Sie auf den runden 2x-Button in der rechten unteren Ecke des Simulators tippen, um die App auf die doppelte Größe zu bringen. Die gute Nachricht ist, dass das eine Menge bestehender iPhone-Apps auf dem iPad nutzbar macht. Das ermöglichte dem neuen Gerät, mit einer Vielzahl von Apps auf dem Markt zu erscheinen. Die schlechte Nachricht ist, dass das Bild dieser automatisch angepassten Apps nicht sonderlich scharf ist. Ein Pixel wird zu vier – zwei in jeder Richtung. Im Screenshot sieht man die Verpixelung wahrscheinlich nicht, aber im Simulator oder auf dem Gerät springt sie einem ins Auge. Sie müssen also nichts tun, damit eine iPhone-App auf dem iPad läuft. Unser nächster Schritt erfordert ebenfalls nicht viel Arbeit und führt zu einer Aepp, die den Bildschirm des iPads in voller Auflösung ausnutzt.
1.9
Umwandlung in eine Universal-App Noch haben Sie nur eine iPhone-App. Gut, Sie haben gesehen, dass Sie diese auf dem iPhone und dem iPad ausführen können, aber dort wirkt sie nicht nativ. Selbst wenn sich Ihre Benutzer an die Unschärfe gewöhnt haben, werden sie wahrscheinlich bemerken, dass die App
W
15
16 X Kapitel 1: Vom iPhone zum iPad nicht den gesamten Bildschirm einnimmt. Zum Teil liegt das daran, dass das Seitenverhältnis der Bildschirme von iPhone und iPad nicht übereinstimmt. Wenn Sie explizit für iPhone und iPad entwickeln, haben Sie drei grundlegende Möglichkeiten: 앫
Sie gehen von einem singulären Projekt mit einer Zielplattform aus und erstellen eine universelle App. So erzeugen Sie eine Binärdatei, deren iPhone-Version auf dem iPhone und deren iPad-Version auf dem iPad läuft. Der Benutzer erwirbt und lädt eine einzige App, die sich, abhängig davon, auf welchem Gerät sie installiert ist, unterschiedlich verhält. Wenn Sie für beide Plattformen im Wesentlichen die gleiche App schreiben, sollten Sie dieses Verfahren wählen.
앫
Unterscheiden sich iPhone- und iPad-App aus Nutzerperspektive, teilen aber eine Menge Code, sollten Sie zwei Binärdateien erzeugen. Anders gesagt, Sie haben ein Projekt mit zwei Zielplattformen.
앫
Wenn sich iPhone- und iPad-App aus Nutzerperspektive unterscheiden und keine Dateien gemeinsam haben, sollten Sie wahrscheinlich zwei separate Projekte erstellen. Diese Option ist am wenigsten erstrebenswert.
Wir entscheiden uns für die erste Möglichkeit und wandeln unsere iPhone-App in eine universelle App um, damit sie auf dem iPad und dem iPhone läuft. Noch werden wir auf beiden Plattformen genau die gleiche App ausführen. Im nächsten Kapitel werden Sie erfahren, wie Sie das Look-and-Feel für die jeweilige Plattform anpassen. Wählen Sie in Xcode das Target DailyShoot, klicken Sie mit rechts darauf und wählen Sie „Upgrade current target for iPad”.
Umwandlung in eine Universal-App Diese Option finden Sie auch als Menüoption im Project-Menü. Egal welchen Weg Sie wählen, Sie haben die Möglichkeit, eine einzige, universelle App oder zwei gerätespezifische Apps zu erstellen. Entscheiden Sie sich dafür, eine universelle App zu erstellen, und klicken Sie auf den OK-Button. Es gibt zwei sichtbare Änderungen. Sie werden erkennen, dass eine neue Gruppe für Ihre iPad-Ressourcen erstellt wurde, die eine neue Nib-Datei enthält.
Klicken Sie doppelt auf die MainWindow-iPad-Nib, um sie im IB zu öffnen, sehen Sie, dass ihre Struktur der der MainWindow-Nib entspricht. Die Ausmaße des Fensters sind anders. Wenn Sie die App jetzt als iPad-App erstellen und ausführen, füllt die Anwendung den gesamten Bildschirm, aber die Auflösung ist die gleiche wie bei der ursprünglichen nativen iPhone-App.
W
17
18 X Kapitel 1: Vom iPhone zum iPad Wow. Das sieht ja entsetzlich aus. Schauen Sie sich nur den verschwendeten Platz an. Wählen Sie einen der Aufträge aus, sieht die Detailseite gut aus. Sie sehen den Browser in vollständiger Größe und Auflösung. Aber dieses Beispiel zeigt Ihnen, warum es keine Vorlage für eine navigationsbasierte iPad-App gibt. Im nächsten Kapitel werden wir einen Split-View nutzen, um dieses Problem zu lösen.
MainWindow-iPad prüfen Manchmal funktioniert das Upgrade zu einer universellen App nicht richtig. Sie sollten sicherstellen, dass die resultierende Nib tatsächlich eine iPad-Window-Nib und keine iPhone-Nib ist. Klicken Sie doppelt in das Window-Objekt in der Nib. Es öffnet sich ein Fenster, das dieses Objekt enthält. Hat es die Größe eines iPhone-Fensters, müssen Sie die Nib aktualisieren. Wählen Sie das Document-Fenster und dann das Menüelement File → Create iPad Version. Sie müssen die iPhone-Version löschen und die iPad-Version als MainWindowiPad.xib speichern.
1.10
Ein paar Striche ergänzen Wir sollten noch ein paar kleine Änderungen vornehmen, bevor wir fortfahren. Das App-Symbol für das iPad hat die Größe 72×72 Pixel, das für das iPhone 57×57 Pixel bzw. 114×114 Pixel für das Retina-Display des iPhone 4. Außerdem gibt es Unterschiede zwischen den SplashScreens, die beim Start angezeigt werden. Erstens müssen wir dafür sorgen, dass unterschiedliche Bilder angezeigt werden, je nachdem, ob die App im Landscape- oder im Portrait-Modus gestartet wird. Schließlich müssen wir sicherstellen, dass der Anwender die App im Landscape-Modus überhaupt starten kann. Noch haben wir kein spezielles Symbol und keinen Splash-Screen für die iPhone-Version unserer App, deswegen werden wir uns gleichzeitig um die iPhone- und iPad-Version kümmern. Wenn Sie nicht gerade ein begnadeter Designer sind und einen Teil Ihrer Entwicklungszeit mit derartigen Arbeiten verbringen wollen, sollten Sie sich einen Designer suchen, der Sie bei den entsprechenden Arbeiten für Ihre App unterstützt. iPad-Besitzer erwarten, dass Apps ordentlich gestaltet sind. Ein schlechtes Design sorgt dafür, dass Ihre App auf weniger erstrebenswerte Weise wahrgenommen wird.
Ein paar Striche ergänzen
Separate Symbole Erstellen wir zwei einfache Symbole für unsere App. Sie können ein beliebiges Bild nehmen. Komponieren Sie ein Bild mit einer Kamera oder etwas anderem, das den Gedanken hinter der Daily Shoot-Website widerspiegelt. Wir haben ein einfaches Symbol erstellt, indem wir einen Screenshot eines Teils der Titelseite des Buchs aufgenommen und als PNG-Datei gespeichert haben. Sie müssen Ihr Bild in zwei unterschiedlichen Größen speichern.6 Nennen Sie die iPhone-Version icon.png und achten Sie darauf, dass es 57×57 Pixel hat. Nennen Sie analog die iPadVersion icon-ipad.png und achten Sie hier auf 72×72 Pixel. Ziehen Sie die beiden PNG-Dateien unter die Resources-Gruppe Ihres Xcode-Projekts. Wenn Sie dazu aufgefordert werden, wählen Sie „Copy items into destination group’s folder (if needed)”. Öffnen Sie die DailyShoot-Info.plist in einem Texteditor und ersetzen Sie das Element mit dem Schlüssel CFBundleIconFile durch folgendes Element:7 CFBundleIconFiles <array> <string>icon <string>icon-ipad
Lassen Sie die Dateinamenserweiterungen weg. Das System sucht dann nach der passenden Version für das jeweilige Display und wählt beim iPhone 4 automatisch die Icon-Datei mit der hohen Auflösung, wenn eine solche vorhanden ist. Geben Sie Dateinamenserweiterungen an, müssen Sie alle Icon-Dateien in der Liste aufführen. Bereinigen Sie Ihr Target, beenden Sie den Simulator, erstellen Sie Ihre App und führen Sie sie aus. Dann sollten Sie die entsprechenden Symbole sehen. Hier sehen Sie Ihres im Dock mit einigen anderen Symbolen für Apps, die Apple stellt.
6 Wenn Sie auch für das iPhone 4 entwickeln wollen, brauchen Sie drei Versionen. Nennen Sie die Version für das iPhone 4 [email protected] und achten Sie darauf, dass sie 114×114 Pixel groß ist. 7 Sie können natürlich auch den PList-Editor nutzen, um diese Ergänzung vorzunehmen, aber anhand von Code ist diese Vorgehensweise besser zu demonstrieren.
W
19
20 X Kapitel 1: Vom iPhone zum iPad Apple empfiehlt, zusätzlich ein 50×50 Pixel großes Symbol zu erstellen und es Icon-Small-50.png zu nennen, das von Spotlight auf dem iPad verwendet wird, sowie ein 29×29 Pixel großes Symbol namens IconSmall.png (bzw. ein 58×58 Pixel großes Symbol namens [email protected] für das iPhone 4), das von Spotlight auf dem iPhone verwendet wird. Diese Symbole sind erforderlich, wenn Sie Settings nutzen. Wir werden sie nicht einsetzen, aber wenn Sie es möchten, müssen Sie darauf achten, dass Sie sie ebenfalls der CFBundleIconFiles-Liste in der PList hinzufügen.
Splash-Screens Unsere Anwendung startet so schnell, dass man eigentlich keinen Splash-Screen benötigt. Erstellen wir trotzdem einen, damit Sie sehen, wie man das macht. Erstellen Sie einige einfache Splash-Screens für die Anwendung. Es könnte beispielsweise einfach das Bild des Startfensters sein. Wir nutzen für unser Beispiel die Zeichenfolge „The Daily Shoot” mit den Farben der Website. Diesmal müssen wir drei unterschiedliche Dateien erstellen. Setzen Sie die Größe für den Splash-Screen für die iPhone-App auf 320×460 Pixel und speichern Sie ihn unter dem Namen Default.png.8 Für das iPad benötigen wir eine Portraitversion mit 768×1004 Pixeln und dem Namen Default-Portrait.png und eine Landscape-Version mit 1.024×748 Pixeln namens Default-Landscape.png.
Ziehen Sie die drei Dateien in die Gruppe Resources, damit die SplashScreens beim Start angezeigt werden können. 8 Wenn Sie auch für das iPhone 4 und sein Retina-Display entwickeln wollen, benötigen Sie einen Splash-Screen, der 640×920 Pixel groß ist und unter dem Namen [email protected] gespeichert wird.
Zusammenfassung
Mehrere Orientierungen beim Start Läuft die App, stellen wir sicher, dass sie in beiden Orientierungen laufen kann, indem wir in jedem unserer View-Controller YES aus shouldAutorotateToInterfaceOrientation: liefern. Soll die App jedoch auch in beiden Orientierungen starten können, müssen Sie folgendes Element der DailyShoot-Info.plist hinzufügen: UISupportedInterfaceOrientations~ipad <array> <string>UIInterfaceOrientationPortrait <string>UIInterfaceOrientationPortraitUpsideDown <string>UIInterfaceOrientationLandscapeLeft <string>UIInterfaceOrientationLandscapeRight
iPhone-Apps starten immer im Portrait-Modus. Indem Sie die unterstützten Schnittstellenorientierungen in der PList angeben, ermöglichen Sie der iPad-App den Start in jeder eingeschlossenen Orientierung. Es ist leicht, eigene Symbole, Splash-Screens und zusätzliche Orientierungen einzubauen. Diese kleinen Änderungen machen für Ihre Endanwender aber einen gewaltigen Unterschied. Wir haben jetzt Splash-Screens und Symbole für beide Geräte und die App so eingerichtet, dass sie auf dem iPad in jeder Orientierung starten kann.
1.11
Zusammenfassung Als iPhone- und iPad-Entwickler wenden wir uns explizit gegen den Gedanken des „Write once, run anywhere”.9 Wir nutzen wenn möglich eine gemeinsame Codebasis, aber unsere Design- und Entwicklungsentscheidungen werden davon gesteuert, was dem Anwender, der auf einem spezifischen Gerät eine spezifische Aufgabe erfüllen will, den größten Nutzen bringt. Die universelle Version unserer App nutzt das iPad nicht optional. Sie ist immer noch für das iPhone gebaut und nur so angepasst, dass sie auf dem iPad okay aussieht. Versuchen wir als Gedankenexperiment einmal, einige kleine Änderungen an dem Bild vorzunehmen, mit dem wir zu Anfang die Größe der iPhone- und iPad-Bildschirme verglichen haben, um eine App zu erstellen, die wie eine iPhone-App funktioniert, aber den zusätzlichen Bildschirmplatz des iPads nutzt.
9 Das war eines der wichtigsten Argumente für die Entwicklung von Anwendungen in der Programmiersprache Java.
W
21
22 X Kapitel 1: Vom iPhone zum iPad
Im Landscape-Modus können wir auf dem iPad den Navigations-View und den Browser gleichzeitig anzeigen. Stellen Sie sich vor, dass wir nun den Navigations-View so strecken, dass er die linke Seite des Bildschirms einnimmt, und dem Browser den restlichen Platz gönnen. Genau das werden wir im nächsten Kapitel machen, wenn wir den Split-View nutzen werden. Und was ist mit dem Portrait-Modus?
Zusammenfassung Sie sehen, dass wir in dieser Ausrichtung nicht einmal den ursprünglichen iPhone-Bildschirm auf dem iPad-Bildschirm unterbringen können. Wenn wir sie vertikal ausdehnten, sähe die Kombination gleichermaßen überfüllt und verzerrt aus. Deswegen zeigt der Split-View im PortraitModus nur den Browser und nicht die Navigation. Folgen Sie Apples Empfehlungen und feilen Sie an allen Aspekten Ihrer Schnittstelle für die verschiedenen Geräte. Trotz dieser Einschränkung sollten Sie sich bewusst machen, wie weit wir mit einfachen Mitteln gekommen sind: Wir haben eine universelle App erstellt, die auf dem iPhone und dem iPad läuft. Dabei haben wir uns einige der Kernkonzepte angesehen, die die Programmierung für beide Geräte mit dem iOS SDK gemeinsam haben. Mehr zur Erstellung universeller Apps und den verschiedenen PList-Eigenschaften finden Sie in Apples iOS Application Programming Guide [App10a]. Beginnen wir jetzt damit, mit einem iPad-spezifischen Widget zu arbeiten: dem UISplitViewController.
W
23
Kapitel 2
Split-Views In diesem Kapitel wollen wir uns ansehen, wie man eine Anwendung erstellt, die sich anfühlt, als wäre sie explizit für das iPad entworfen worden. Dazu werden wir zwei iPad-spezifische Controller nutzen: den UISplitViewController und den UIPopoverController. Der Split-View-Controller ermöglicht uns, den Navigations- und den Detail-View auf einem geteilten Bildschirm anzuzeigen. In diesem Kapitel werden Sie den Split-View-Controller nutzen, um Navigation und Details gleichzeitig anzuzeigen.
Das funktioniert ausgezeichnet, solange der Benutzer das iPad horizontal hält, sieht aber entsetzlich aus, wenn er das Gerät dreht und verti-
26 X Kapitel 2: Split-Views kal vor sich hat. Wie Sie im letzten Kapitel sahen, gibt es nicht genug Platz, um beides auf einmal anzuzeigen. Die Lösung ist, dass der Navigations-View ausgeblendet und nur der Detail-View angezeigt wird. Wenn der Anwender die Navigation benötigt, kann er das iPad drehen, oder einen Button drücken und ein übergelagertes Menü öffnen, das als Popover bezeichnet wird.
Hier ist der Plan. Wir werden in die iPad-Version unserer Anwendung einen Split-ViewController einführen. Dabei dürfen wir allerdings nicht vergessen, dass es eine universelle Anwendung ist. Während wir an der iPad-Version arbeiten, muss die iPhone-Version funktionsfähig bleiben. Würden wir nur das iPad anvisieren, könnten wir einfach ein neues Projekt mit der Split-View-basierten App-Vorlage beginnen. Das überlassen wir Ihnen jedoch hier als Übung am Ende des Kapitels. Jedenfalls wird die Anwendung, nachdem wir den Split-View-Controller eingerichtet haben, richtig aussehen, egal in welcher Ausrichtung sie
Den Split-View-Controller im IB einführen W 27 gestartet wird, sie verhält sich aber nicht in beiden Fällen korrekt. Damit der Landscape-Modus richtig funktioniert, müssen wir eine Kommunikation zwischen dem Navigations-View und dem Detail-View einrichten, die dafür sorgt, dass, sobald der Anwender auf einen Auftrag im Navigations-View klickt, die diesem Auftrag entsprechende Webseite im Detail-View angezeigt wird. Damit der Portrait-Modus richtig funktioniert, müssen wir einen Popover-View einfügen, der die Aufträge anzeigt, wenn der Anwender sie benötigt, und das Popover wieder entfernt, wenn der Anwender sie nicht mehr benötigt.
2.1
Den Split-View-Controller im IB einführen In einer navigationsbasierten App für das iPhone präsentieren Sie dem Anwender einen Table-View. Wählt der Anwender eine der Zeilen, gelangt er entweder zu einem anderen Table-View oder einem DetailView für das ausgewählte Element. Aufgrund der Größenbeschränkungen des iPhones wird jeder dieser Views über den vorangehenden gezeichnet. Das analoge Gegenstück auf dem iPad ist die Split-View-basierte App. Im Landscape-Modus erscheint der Table-View auf der linken Seite. Wählt der Anweder eine der Zeilen aus, gelangt er entweder zu einem weiteren Table-View oder sieht den Detail-View für das ausgewählte Element. Der Unterschied ist, dass ein weiterer Table-View wieder in der linken Hälfte des Bildschirms angezeigt wird, während bei der Auswahl eines Elements einfach der Detail-View aktualisiert wird, der immer auf der rechten Seite zu sehen ist. Anders ausgedrückt, der Detail-View ändert sich, wenn der Anwender mit ihm interagiert oder wenn er zu einem anderen Abschnitt geht. Öffnen Sie die MainWindow-iPad-Nib.
28 X Kapitel 2: Split-Views Das Document-Fenster sieht aktuell aus wie das Document-Fenster für die MainWindow-Nib, die in der iPhone-Version verwendet wird. Das ändern wir jetzt. Wählen Sie den Navigations-Controller und löschen Sie ihn entweder, indem Sie die Löschen-Taste drücken oder die Menüoption Edit → Delete wählen. Suchen Sie in der Library den Split-ViewController und ziehen Sie ihn ins Document-Fenster, ihn dem sich zuvor der Navigations-Controller befand.1 Klicken Sie auf die Dreiecke vor den Elementen, sehen Sie, dass der Split-View-Controller zwei Controller enthält: einen Navigations-Controller und einen View-Controller. Wir müssen einige Anpassungen vornehmen, um sicherzustellen, dass wir unsere angepassten Klassen laden. Wählen Sie den View-Controller und nutzen Sie den Identity-Inspektor, um seine Klasse in AssignmentViewController zu ändern. Und da Sie gerade einmal dort sind, nutzen Sie den Attributes-Inspektor, um den NIB-Namen in AssignmentViewController-Nib zu ändern. Wählen Sie auf gleiche Weise den Table-View-Controller unter dem NavigationsController. Verwenden Sie den Identity-Inspektor, um den Typ in RootViewController zu ändern, und ändern Sie im Attributes-Inspektor den NIB-Namen ebenfalls in RootViewController.
Wir müssen eine Verbindung reparieren, die wir zerstört haben, als wir den ursprünglichen Navigations-Controller löschten. Wählen Sie das App-Delegate und nutzen Sie den Connections-Inspektor, um das navigationController-Outlet mit dem neuen Navigations-Controller zu verbinden. Speichern Sie Ihre Arbeit. Erstellen Sie die App und führen Sie sie aus. 1 Wenn Sie den Split-View-Controller nicht im Objects-Tab der Library finden, ist Ihre Nib eine iPhone-Nib und keine iPad-Nib. Gehen Sie in das Document-Fenster, wählen Sie die Menüoption File → Create iPad Version und ersetzen Sie die vorhandene MainWindowiPad.xib-Datei durch die neu erstellte Datei.
Mit dem Split-View-Controller interagieren W 29 Überrascht? Die App läuft genau wie gegen Ende des letzten Kapitels. Wo ist unser Split-View? Die Antwort ist natürlich, dass wir noch einige Anpassungen am Code vornehmen müssen. Als iOS-Entwickler verbringen wir unser Leben damit, zwischen Xcode und Interface Builder hin- und herzuhüpfen. Springen wir wieder zu Xcode zurück und ändern wir, was beim Start passiert.
2.2
Mit dem Split-View-Controller interagieren Warum hat sich nichts geändert? Sie werden die Antwort in der Methode application:didFinishLaunchingWithOptions:2 finden. SplitVC/DailyShoot3/Classes/DailyShootAppDelegate.m
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [self.window addSubview:self.navigationController.view]; [self.window makeKeyAndVisible]; return YES; }
Obwohl wir jetzt im IB einen Split-View eingerichtet haben, fügen wir immer noch den View des Navigations-Controllers als unseren HauptView ein. Das reparieren wir, indem wir ein Outlet für den Split-ViewController hinzufügen, den wir splitVC nennen werden. Fügen Sie dem App-Delegate dieses Outlet hinzu. Ab hier werden wir voraussetzen, dass Sie wissen, dass dazu die folgenden Schritte erforderlich sind: 1. In DailyShootAppDelegate.h: Deklarieren Sie eine Eigenschaft des Typs UISplitView-Controller namens splitVC mit den Attributen nonatomic und retain und deklarieren Sie die Eigenschaft als IBOutlet.3 Speichern Sie. 2. Im IB: Nutzen Sie den Connections-Inspektor, um dieses neue Outlet mit der UISplitViewController -Instanz zu verbinden. Speichern Sie. 3. Synthetisieren Sie die neue Eigenschaft in DailyShootAppDelegate.m. 4. Bereinigen Sie den Speicher, indem Sie der Methode dealloc die Zeile [splitVC release], splitVC=nil; hinzufügen. Wir haben die Vorlage so modifiziert, dass statt window self.window und statt navigationController self.navigationController aufgerufen wird, damit wir die Akzessor-
2
methoden nutzen, statt direkt die ivars anzusprechen. 3 Wir setzen voraus, dass Sie mit der aktuellsten Version des Simulators arbeiten und den entsprechenden ivar nicht mehr deklarieren müssen. Denken Sie auch daran, dass diese Variable eigentlich ein Zeiger auf eine UISplitViewController -Instanz ist, auch wenn wir splitVC vereinfachend als UISplitViewController bezeichnen.
30 X Kapitel 2: Split-Views Denken Sie daran, dass Sie Ihre Arbeit immer mit der aktuellen Phase des Codes vergleichen können, den wir erstellen. Für dieses Beispiel können Sie sich den Download-Code im Verzeichnis DailyShoot4 ansehen. Apple empfiehlt, dass Sie folgende Prüfung durchführen, um zu bestimmen, ob Ihr Code auf dem iPad oder auf dem iPhone läuft: UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad
Hier werden wir den View des Split-View-Controllers als obersten View einfügen, wenn der Code auf dem iPad läuft, und wir nutzen den View des Navigations-Controllers, wenn der Code auf dem iPhone läuft. SplitVC/DailyShoot4/Classes/DailyShootAppDelegate.m
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { [self.window addSubview:self.splitVC.view]; } else { [self.window addSubview:self.navigationController.view]; } [self.window makeKeyAndVisible]; return YES; }
Erstellen Sie die App und führen Sie sie aus. Sie sollten in etwa folgendes Ergebnis sehen:
Mit dem Split-View-Controller interagieren W 31 Das sieht ja schon ganz ansehnlich aus. Testen Sie es in unterschiedlichen Ausrichtungen, sehen Sie, wie der Navigationsteil der Seite vom und auf den Bildschirm gleitet, wenn das Gerät gedreht wird. Dreht sich die App sich nicht, wenn Sie das Gerät drehen, sollten Sie prüfen, ob die Methode shouldAutorotateToInterfaceOrientation: in RootViewController.m und AssignmentViewController.m gleichermaßen YES liefert. Wie so häufig der Fall, gibt es gute und schlechte Nachrichten. Die gute Nachricht ist, dass der Detail-View des Split-View mit der standardmäßigen Auftragsseite gefüllt wird, bevor der Benutzer einen bestimmten Auftrag ausgewählt hat. Sie können aus der viewDidLoad:-Methode in AssignmentViewController.m sehen, dass das eine zufällige Folge des Versuchs ist, die Seite http://dailyshoot.com/assignments/ ohne irgendetwas nach dem letzten Schrägstrich zu laden. Wie bereits in der Einführung zu diesem Kapitel erwähnt, müssen wir einige Probleme im Landscape-Modus und einige andere Probleme im Portrait-Modus lösen. Wir werden mit den Maßnahmen für den Landscape-Modus beginnen. Wo es hakt, sehen Sie, wenn Sie einen der Aufträge im Navigations-View auf der linken Seite auswählen.
32 X Kapitel 2: Split-Views Das ist das Verhalten, das wir in der iPhone-Version erwarten. Wir schieben den Detail-View für das im Table-View ausgewählte Element über den Table-View, statt den Table-View sichtbar zu lassen, und wir aktualisieren die auf der rechten Seite angezeigte Webseite. Machen wir uns daran, dieses Problem zu reparieren.
2.3
Zwischen den View-Controllern kommunizieren Beginnen wir damit, eine zweiseitige Verbindung zwischen den Controllern herzustellen, die für die beiden Teile des Split-View verantwortlich sind. Erstellen Sie in der Klasse AssignmentViewController ein Outlet namens rootVC des Typs RootViewController und in der Klasse RootViewController ein Outlet namens assignmentVC des Typs AssignmentViewController. Die einzige Nib, die beide Objekte enthält, ist die MainWindow-iPad-Nib. Erstellen Sie dort Ihre Verbindungen. Wir haben mehrere Möglichkeiten, wenn wir das richtige Verhalten für beide Plattformen auf einmal implementieren wollen. Schauen wir uns zwei Varianten zur Anpassung des Delegates des Table-View an, die davon abhängen, auf welchem Gerät die App läuft. Erst werden wir den RootViewController so anpassen, dass er als Delegate für den TableView auf beiden Geräten dient. Im nächsten Abschnitt werden wir ein weiteres Verfahren testen, bei dem wir einen Satz paralleler View-Controller für die iPhone- und iPad-Implementierungen nutzen werden. Sie sollten beide Verfahren verstehen, aber im restlichen Kapitel werden wir mit der zweiten Fassung weiterarbeiten. Aktuell ist das RootViewController -Objekt das Delegate für den TableView. Wir können die Delegate-Methode tableView:didSelectRowAtIndexPath: folgendermaßen anpassen:4 SplitVC/DailyShoot5/Classes/RootViewController.m
X X X X
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { self.assignmentVC.assignmentNumber = [self.assignments assignmentAtIndex:indexPath.row]; [self.assignmentVC loadSelectedPage];
4
Wenn Sie Compiler-Warnungen oder -Fehler erhalten, sollten Sie prüfen, ob Sie die
@class-Vorabdeklarationen und imports eingefügt und auch die Methode loadSelectedPage implementiert haben.
Unterschiedliche Geräte mit Unterklassen berücksichtigen W 33 } else { AssignmentViewController *detailViewController = [[AssignmentViewController alloc] initWithNibName:@"AssignmentViewController" bundle:nil]; detailViewController.assignmentNumber = [self.assignments assignmentAtIndex:indexPath.row]; [self.navigationController pushViewController:detailViewController animated:YES]; [detailViewController release]; }
X
X }
Anders gesagt: Wenn das Gerät ein iPhone ist, wird eine neue Instanz von AssignmentViewController erstellt, konfiguriert und wie zuvor auf den Navigations-Stack geschoben. Läuft die App hingegen auf dem iPad, wird die bestehende AssignmentViewController -Instanz genutzt, konfiguriert, und der Webbrowser wird mit diesen Informationen neu geladen.
2.4
Unterschiedliche Geräte mit Unterklassen berücksichtigen Langsam durchziehen die Prüfungen auf das aktuelle Gerät unseren Code. Vergessen Sie nicht, dass Sie erheblich mehr Zeit mit der Wartung als mit dem Schreiben des Codes verbringen werden. Das Verhalten auf iPhone oder iPad wird bei all den eingestreuten if-Anweisungen in einem größeren Projekt schwer nachvollziehbar. Dies ist nicht nur ein iPad-spezifischer Rat: Wenn Sie den Eindruck gewinnen, dass Ihr Code mit Bedingungsprüfungen übersäht ist, sollten Sie Ihre üblichen Werkzeuge einsetzen, um Methoden und Klassen so aufzuteilen, dass der Zweck des Codes klarer wird. Ein robusteres Verfahren wäre es, die Klassendateien in drei verschiedene Kategorien aufzuspalten. Der Code, den beide Plattformen teilen, kommt in gemeinsame Dateien. Code, der für das iPhone oder das iPad angepasst werden muss, kommt in spezifische, davon abgeleitete Unterklassen. Am Ende des nächsten Abschnitts wird unser Quellcode aus den folgenden Dateien bestehen:5
5 Sie können drei neue Gruppen namens Shared, iPad und iPhone erstellen und die entsprechenden Dateien in sie verschieben.
34 X Kapitel 2: Split-Views
Betrachten Sie zunächst den RootViewController. In ihm wird das Verhalten nur in der Implementierung der tableView:didSelectRowAtIndexPath:-Methode des Table-View-Delegates gerätespezifisch angepasst. Wir werden diese Methode aus der Klasse RootViewController herausziehen und zwei Unterklassen erstellen, die sie implementieren. Erstellen Sie eine RootViewController -Unterklasse namens RootViewController_Pad. Die Header-Datei ist ziemlich schlank. SplitVC/DailyShoot5/Classes/RootViewController.m
#import "RootViewController.h" @interface RootViewController_Pad : RootViewController { } @end
Auch die Implementierungsdatei ist nicht sehr umfangreich. Wir implementieren die Delegate-Methode mit iPad-spezifischem Code. Sie können die entsprechenden Zeilen aus der tableView:didSelectRowAtIndex Path:-Methode in RootViewController.m kopieren. SplitVC/DailyShoot6/Classes/RootViewController_Pad.m
#import "RootViewController_Pad.h" #import "Assignments.h" #import "AssignmentViewController.h" @implementation RootViewController_Pad
Unterschiedliche Geräte mit Unterklassen berücksichtigen W 35 -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { self.assignmentVC.assignmentNumber = [self.assignments assignmentAtIndex:indexPath.row]; [self.assignmentVC loadSelectedPage]; } @end
Für den iPhone-Fall nutzen wir das gleiche Muster. Erstellen Sie eine zweite RootViewController -Unterklasse namens RootViewController_ Phone. SplitVC/DailyShoot6/Classes/RootViewController_Phone.h
#import "RootViewController.h" @interface RootViewController_Phone : RootViewController { } @end
Ihre Implementierung der Delegate-Methode enthält das iPhone-spezifische Verhalten. Kopieren Sie erneut die entsprechenden Zeilen aus der TableView:didSelectRowAtIndexPath:-Methode in RootViewController.m. SplitVC/DailyShoot6/Classes/RootViewController_Phone.m
#import "RootViewController_Phone.h" #import "Assignments.h" #import "AssignmentViewController.h" @implementation RootViewController_Phone -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { AssignmentViewController *detailViewController = [[AssignmentViewController alloc] initWithNibName:@"AssignmentViewController" bundle:nil]; detailViewController.assignmentNumber = [self.assignments assignmentAtIndex:indexPath.row]; [self.navigationController pushViewController:detailViewController animated:YES]; [detailViewController release]; } @end
Wir haben ohne großen Aufwand das gerätespezifische Verhalten mit Unterklassen von RootViewController aufgeteilt. Löschen Sie die Implementierung von tableView:didSelectRow-AtIndexPath: aus RootViewController.m. Die Oberklasse hat alle Datenquellenmethoden des Table-View, und wir haben die gerätespezifischen Delegate-Methoden in die Unterklassen verschoben.
36 X Kapitel 2: Split-Views Unglücklicherweise weiß der Rest der Anwendung noch nichts von diesen Unterklassen. Teilen wir das App-Delegate in eine iPhone- und eine iPad-spezifische Klasse auf und rufen wir aus jeder die entsprechende RootViewController -Unterklasse auf.
2.5
Die App-Delegates trennen Wir müssen einige weitere Änderungen vornehmen, damit die entsprechenden RootViewController -Unterklassen genutzt werden. Dazu werden wir separate App-Delegates für die beiden Plattformen erstellen. Der Code, den beide Plattformen teilen, bleibt in der gemeinsamen Oberklasse DailyShootAppDelegate. Hier ist die Header-Datei: SplitVC/DailyShoot6/Classes/DailyShootAppDelegate.h
#import @interface DailyShootAppDelegate : NSObject { } @property (nonatomic, retain) IBOutlet UIWindow *window; @property (nonatomic, retain) IBOutlet UINavigationController *navigationController; @end
Die Implementierungsdateien entfernen die Prüfung auf das Zielgerät. Die Hinzufügung des anfänglichen View werden wir erst in den Unterklassen einfügen. Hier ist die DailyShootAppDelegate.m: SplitVC/DailyShoot6/Classes/DailyShootAppDelegate.m
#import "DailyShootAppDelegate.h" @implementation DailyShootAppDelegate @synthesize window, navigationController; - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [self.window makeKeyAndVisible]; return YES; } - (void)dealloc { [navigationController release], navigationController = nil ; [window release], window = nil ; [super dealloc]; } @end
Die App-Delegates trennen W 37 Erstellen Sie eine DailyShootAppDelegate_Phone-Klasse mit folgendem Header: SplitVC/DailyShoot6/Classes/DailyShootAppDelegate_Phone.h
#import #import "DailyShootAppDelegate.h" @interface DailyShootAppDelegate_Phone :DailyShootAppDelegate { } @end
Beim iPhone fügen Sie den View des Navigations-Controllers als obersten View hinzu und rufen die application:didFinishLaunchingWithOptions:-Methode der Oberklasse auf. SplitVC/DailyShoot6/Classes/DailyShootAppDelegate_Phone.m
#import "DailyShootAppDelegate_Phone.h" @implementation DailyShootAppDelegate_Phone - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [self.window addSubview:self.navigationController.view]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; } @end
Erstellen Sie dann gleichermaßen eine DailyShootAppDelegate_PadKlasse mit folgendem Header: SplitVC/DailyShoot6/Classes/DailyShootAppDelegate_Pad.h
#import #import "DailyShootAppDelegate.h" @interface DailyShootAppDelegate_Pad :DailyShootAppDelegate { } @property (nonatomic, retain) IBOutlet UISplitViewController *splitVC; @end
In dieser Unterklasse müssen Sie eine Eigenschaft für den Split-ViewController ergänzen. Fügen Sie in dieser Implementierung den View des Split-View-Controllers als obersten View hinzu und bereinigen Sie das Objekt in der Methode dealloc. SplitVC/DailyShoot6/Classes/DailyShootAppDelegate_Pad.m
#import "DailyShootAppDelegate_Pad.h" @implementation DailyShootAppDelegate_Pad @synthesize splitVC;
38 X Kapitel 2: Split-Views - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [self.window addSubview:self.splitVC.view]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; } - (void)dealloc { [splitVC release], splitVC = nil; [super dealloc]; } @end
Wir sind fast fertig. Sie müssen nur noch ein paar kleine Änderungen an den Nib-Dateien vornehmen. Öffnen Sie die Datei MainWindow.xib im IB, nutzen Sie den Identity-Inspektor, um den Typ des DailyShootAppDelegate-Objekts in DailyShootAppDelegate_Phone und den Typ des RootViewController -Objekts in RootViewController_Phone zu ändern.
Öffnen Sie dann die Datei MainWindow-iPad.xib im Interface Builder, nutzen Sie erneut den Identity-Inspektor, um den Typ des DailyShootAppDelegate-Objekts in DailyShootAppDelegate_Pad zu ändern, und ändern Sie den Typ des RootViewController-Obekts in RootViewController_Pad.
Dem Detail-View eine Werkzeugleiste hinzufügen W 39 Speichern Sie Ihre Änderungen, erstellen Sie die App und führen Sie sie aus. Dann sollte alles wieder laufen wie zuvor. Wow! Für die Ersetzung zweier einfacher if-Anweisungen scheint das eine Menge Aufwand gewesen zu sein. Aber Ihr Code ist jetzt erheblich besser les- und wartbar. Außerdem haben Sie wahrscheinlich festgestellt, dass Sie eigentlich nicht viel tun mussten, um diese Änderungen zu implementieren – nach viel Arbeit sieht das nur aus, weil wir es explizit auf diesen Seiten ausgewalzt haben.
2.6
Dem Detail-View eine Werkzeugleiste hinzufügen Fügen wir der iPad-Version, nicht jedoch der iPhone-Version, jetzt oben in den vom AssignmentViewController verwalteten View eine Werkzeugleiste hinzu. Im Portrait-Modus der iPad-Version werden wir eine solche bald benötigen. Schließen wir diese auch in den LandscapeModus ein, verschafft das unserer App ein einheitlicheres Aussehen. Da die iPhone-Version bereits eine Navigationsleiste bietet, müssen wir darauf achten, dass wir nicht auch dort eine zusätzliche Werkzeugleiste einfügen. Wir werden eine iPad-Version der Klasse AssignmentViewController mit einer entsprechenden Nib erstellen. Wählen Sie die iPad-Gruppe und erstellen Sie eine neue Datei, die eine Unterklasse von UIViewController ist. Achten Sie darauf, dass Sie beide Kästchen angekreuzt haben, um das iPad als Target zu setzen und die XIB zu erstellen. Nennen Sie die neue Klasse AssignmentViewController_Pad. Fügen Sie der Klasse AssignmentViewController_Pad eine Eigenschaft namens toolbar mit dem Typ UIToolbar hinzu und markieren Sie diese als Outlet. Ändern Sie die Oberklasse vom allgemeinen UIViewController in AssignmentViewController. SplitVC/DailyShoot7/Classes/AssignmentViewController_Pad.h
#import "AssignmentViewController.h"
@interface AssignmentViewController_Pad : AssignmentViewController { } @property(nonatomic, retain) IBOutlet UIToolbar *toolbar; @end
Öffnen Sie die AssignmentViewController_Pad-Nib-Datei. Positionieren Sie oben in den View eine UIToolbar und füllen Sie den Rest mit einem UIWebView. Entfernen Sie den Button aus der Werkzeugleiste. Später
40 X Kapitel 2: Split-Views werden wir einen eigenen erstellen. Wählen Sie im Attributes-Inspektor des Web-View „Scales Pages to Fit“. Verbinden Sie die beiden Outlets des File’s Owner mit diesen Elementen und speichern Sie. Noch ist an unserer Implementierungsdatei für die Klasse AssignmentViewController_Pad nicht viel dran: SplitVC/DailyShoot7/Classes/AssignmentViewController_Pad.m
#import "AssignmentViewController_Pad.h" @implementation AssignmentViewController_Pad @synthesize toolbar; - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } - (void)dealloc { [toolbar release], toolbar = nil; [super dealloc]; } @end
Wie zuvor müssen wir an der MainWindow-iPad-Nib einige Modifizierungen vornehmen. Ändern Sie über den Identity-Inspektor den Typ des AssignmentViewController -Objekts in AssignmentViewController_Pad. Außerdem müssen Sie über den Attributes-Inspektor den NIB-Namen in AssignmentViewController_Pad ändern. Erstellen Sie die App und führen Sie sie aus, damit Sie einen Blick auf die neu erstellte Werkzeugleiste werfen können. Wir werden diese Werkzeugleiste gleich nutzen, wenn wir mit dem Split-View und einem neuen iPad-Widget arbeiten, das als Popover bezeichnet wird.
2.7
Das Split-View-Delegate Unsere Anwendung sieht jetzt gut aus und funktioniert auch so – zumindest im Landscape-Modus. Schauen Sie, was geschieht, wenn wir sie auf die Seite legen. Die Navigation verschwindet, und es gibt kein vernünftige Möglichkeit mehr, zu einem der ersten 125 Aufträge zu gehen.
Das Split-View-Delegate W 41
42 X Kapitel 2: Split-Views Glücklicherweise stellt Apple uns ein Mittel zur Verfügung, Benutzern die entsprechende Möglichkeit anzubieten, das darüber hinaus auch noch äußerst leicht zu implementieren ist. Wenn der Benutzer das iPad in Landscape-Ausrichtung hält, soll die App genau so erscheinen, wie sie es jetzt tut. Wechselt er in die PortraitAusrichtung, fügen wir der Werkzeugleiste einen Aufträge-Button hinzu. Drückt der Benutzer auf diesen Button, erhält er eine Aufstellung der Aufträge und kann wie zuvor einen davon auswählen. Hat er einen Auftrag ausgewählt (oder ändert er seine Absicht und wählt keinen aus), müssen wir die Liste wieder entfernen. Der Split-View-Controller steuert zwei Subcontroller. Wird das iPad gedreht, zeigt oder versteckt er den von der RootViewController_PadInstanz gesteuerten View und ändert Position und Größe des View, der vom AssignmentViewController_Pad-Objekt gesteuert wird. Der von AssignmentViewController_Pad verwaltete View ist der, der bei dieser Neupositionierung geändert wird. Das heißt, dass das AssignmentViewController_Pad-Objekt als Delegate für den UISplitViewController dienen wird. Fügen Sie der AssignmentViewController_Pad.h-Header-Datei das Protokoll UISplitViewControllerDelegate hinzu. Knüpfen Sie jetzt das Delegate an. Öffnen Sie die MainWindow-iPad-Nib und verbinden Sie das delegate-Outlet des UISplitViewController mit dem AssignmentViewController_Pad-Objekt. Zwei der Methoden im UISplitViewControllerDelegate-Protokoll werden vom System aufgerufen, wenn das Gerät die Ausrichtung ändert. Große Teile der Arbeit, die geleistet werden muss, wird Ihnen abgenommen. In der Tat sind in den nächsten beiden Abschnitten häufig die Namen der Methoden länger als ihre Implementierungen! Wir beginnen mit der, die aufgerufen wird, wenn wir das iPad vertikal ausrichten.
2.8
Ein Popover einfügen Das iPad bietet ein neues Widget namens Popover, das natürlich von einem Popover-Controller verwaltet wird. Es ist eine Art Menü, das aber nicht an den Button gebunden ist, den Sie drücken, um es erscheinen zu lassen. Wie Sie in Kapitel 4, Popover und modale Dialoge, sehen werden, können Sie Popover auch erscheinen lassen, wenn der Benutzer einen bestimmten Bereich des Bildschirms berührt. Im Augenblick werden wir das Popover erscheinen lassen, wenn ein Button gedrückt wird.
Ein Popover einfügen W 43 Wir werden eine Methode im UISplitViewControllerDelegate-Protokoll implementieren, um der Werkzeugleiste einen Button hinzuzufügen, der, wenn er gedrückt wird, das Popover einblendet, das alle Elemente aus dem Navigations-View enthält. Fügen Sie AssignmentViewController_Pad.h eine Eigenschaft namens popoverController mit dem Typ UIPopoverController hinzu. Eigentlich benötigen wir diese Variable im nächsten Schritt, in dem wir den Popover-Controller hinzufügen, noch nicht, aber wir werden sie brauchen, wenn wir sichern, dass wir das Popover entfernen. Gehen Sie zur Dokumentation des UISplitViewControllerDelegate, kopieren Sie die Signatur dieser Methode und fügen Sie sie ein: - (void)SplitViewController:(UISplitViewController*)svc willHideViewController:(UIViewController *)aViewController withBarButtonItem:(UIBarButtonItem*)barButtonItem forPopoverController:(UIPopoverController*)pc
Ja, Sie haben richtig verstanden: kopieren und einfügen. Das ist eine Delegate-Methode, und wenn Sie nur den winzigsten Tippfehler machen, wird die Methode zur Laufzeit nicht aufgerufen, ohne dass Sie einen Fehler oder eine Warnung sehen, die Ihnen den Grund dafür mitteilt. Diese Methode wird aufgerufen, wenn der Benutzer vom Landscape- zum Portrait-Modus wechselt und der View des RootViewController_Pad verborgen werden soll. Beachten Sie, dass diese Methode als zweiten Parameter Handles auf diesen View-Controller übergibt und den Button auf der Werkzeugleiste als dritten. Der letzte Parameter der Methode ist ein Popover-Controller. Setzen Sie zur Implementierung der Methode den Titel für den Button, zeigen Sie den Button auf der Werkzeugleiste an und setzen Sie den Wert der Eigenschaft popoverController. SplitVC/DailyShoot8/Classes/AssignmentViewController_Pad.m
-(void)SplitViewController:(UISplitViewController*)svc willHideViewController:(UIViewController *)aViewController withBarButtonItem:(UIBarButtonItem*)barButtonItem forPopoverController:(UIPopoverController*)pc { barButtonItem.title = aViewController.title; [self.toolbar setItems:[NSArray arrayWithObject:barButtonItem] animated:YES]; self.popoverController = pc; }
44 X Kapitel 2: Split-Views Das war es. Probieren Sie es aus. Der Button und das Popover sollten im Portrait-Modus erscheinen. Im Landscape-Modus stimmen die Dinge noch nicht, aber das werden wir gleich reparieren.
Aber Moment mal! Wie bitte kommt es, dass das Popover mit den Aufträgen gefüllt wird, die im RootViewController angezeigt wurden? Das ist eins der Kunststücke, die ein Lächeln auf Ihre Lippen zaubern müssten. Wenn die Delegate-Methode aufgerufen wird, ist der View, der verborgen werden soll – in unserem Fall RootViewController –, einer der Parameter, und der Popover-Controller, den wir mit den Informationen füllen wollen, ist einer der anderen Parameter. So wird Ihr Popover mit dem Inhalt des View gefüllt, der verborgen wird, wenn das iPad gedreht wird. Ziemlich cool.
2.9
Popover und Button entfernen Nachdem Sie Ihre App getestet haben, werden Ihnen wahrscheinlich einige Details aufgefallen sein, um die wir uns kümmern müssen. Erstens: Wenn Sie in der Liste im Popover einen Auftrag wählen, wird
Eine iPad-spezifische, Split-View-basierte App erstellen W 45 die Seite hinter dem Popover geladen, ohne dass das Popover selbst verschwindet. Sorgen wir also dafür, dass das Popover entfernt wird, wenn die Seite geladen wird. SplitVC/DailyShoot8/Classes/AssignmentViewController_Pad.m
-(void) loadSelectedPage { [super loadSelectedPage]; if (self.popoverController) { [self.popoverController dismissPopoverAnimated:YES]; } }
Zweitens: Vielleicht haben Sie ebenfalls bemerkt, dass der Button in der Werkzeugleiste bleibt, wenn die App wieder in den LandscapeModus wechselt. Das reparieren wir, indem wir die folgende DelegateMethode für den Split-View-Controller implementieren: SplitVC/DailyShoot8/Classes/AssignmentViewController_Pad.m
- (void)SplitViewController: (UISplitViewController*)svc willShowViewController:(UIViewController *)aViewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem { [self.toolbar setItems:[NSArray array] animated:YES]; self.popoverController = nil; }
Glückwunsch. Sie haben jetzt eine universelle Anwendung, die auf dem iPhone wie eine native navigationsbasierte App arbeitet, auf dem iPad aber wie eine Split-View-basierte App.
2.10
Eine iPad-spezifische, Split-View-basierte App erstellen Nachdem wir eine universelle App erstellt haben, die auf beiden Plattformen funktioniert, wollen wir jetzt zur Übung von vorn beginnen und ein iPad-spezifische App bauen. Erstellen Sie ein neues Xcode-Projekt auf Basis der Split-View-basierten App-Vorlage. Sie können sehen, dass Sie weniger Nibs und Klassen verwenden werden als im letzten Beispiel. Probieren Sie es selbst. Hier ist eine kurze Skizze, die Ihnen zeigt, wie Sie eine mögliche Lösung implementieren, die so wenige Änderungen wie möglich am von der Vorlage erstellten Code erfordert. Erstellen Sie eine neue Split-View-basierte App namens DailyShoot-iPad. Laden Sie die Quelldateien für die Klasse Assignments. Fügen Sie der Klasse RootViewController ein Outlet für ein Objekt des Typs Assignments namens assignments hinzu. Fügen Sie der MainWindow-Nib ein Objekt des Typs Assignments hinzu und verbinden Sie das Outlet.
46 X Kapitel 2: Split-Views Modifizieren Sie die Datenquellenmethoden im RootViewController so, dass die Werte der Eigenschaft assignments geliefert werden. Der DetailViewController übernimmt die Rolle des AssignmentViewController. Ersetzen Sie sein UILabel mit dem UIWebView. Das müssen Sie mit der DetailView-Nib-Datei und den Quellcodedateien des Controllers tun. Wir werden das detailItem der Vorlage für unsere Auftragsnummer nutzen. Wir können den Typ in RootViewController.m in NSNumber ändern und die Delegate-Methode des Table-View folgendermaßen implementieren: SplitVC/DailyShootiPad/Classes/RootViewController.m
-(void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { self.detailViewController.detailItem = [self.assignments assignmentAtIndex:indexPath.row]; }
Jetzt müssen wir die configureView-Methode in DetailViewController.m implementieren, damit die ausgewählte Webseite angezeigt wird, und viewDidLoad, um beim Start die Liste aller Aufträge anzuzeigen. SplitVC/DailyShootiPad/Classes/DetailViewController.m
-(void)configureView { NSString *url = [NSString stringWithFormat:@"http://dailyshoot.com/assignments/%@", self.detailItem]; [self.webView loadRequest: [NSURLRequest requestWithURL:[NSURL URLWithString:url]]]; } -(void)viewDidLoad { [super viewDidLoad]; [self configureView]; }
Außerdem sollten Sie den Text auf dem Werkzeugleisten-Button in DetailViewController.m und den Titeltext für den RootViewController in Aufträge ändern. Fügen Sie Ihre eigenen Symbole und SplashScreens ein. Die Lösung zu dieser Übung können Sie sich im SplitVC/DailyShootiPad-Verzeichnis der Code-Downloads ansehen.
Zusammenfassung W 47
2.11
Zusammenfassung Wie so häufig in der Cocoa- und iOS-Entwicklung haben wir in diesem Kapitel eine Menge erreicht, ohne dazu viel Code schreiben zu müssen. Wir haben unserem Projekt einen Split-View und den dazugehörigen Controller hinzugefügt und eine Kommunikation zwischen den Subviews über die Controller eingerichtet. Das waren die gleichen Controller, RootViewController und AssignmentViewController, die wir im letzten Kapitel verwendet haben. Ohne große Änderungen haben wir unsere Anwendung von einer auf dem iPad laufenden iPhone-App in eine App mit nativem iPad-Lookand-Feel verwandelt. Wir haben gelernt, wie man testet, auf welchem Gerät die Anwendung läuft, und die Anwendung so gestaltet, dass das Verhalten auf beiden Geräten korrekt ist. Wir haben gesehen, wie man auf eine Änderung der Ausrichtung des Geräts reagiert und wie man diese Orientierungen am besten unterstützt. Wir wissen nun, wie leicht es ist, ein Popover zu implementieren, das mit dem gleichen Inhalt gefüllt ist wie der verborgene Navigations-View. Im verbleibenden Buch werden wir iPad-spezifische Apps erstellen. Wir werden mit den neuen APIs und Techniken arbeiten, die dazu dienen sollen, den zusätzlichen Raum zu nutzen, den dieses neue Gerät bietet. Von hier an können Sie das Buch in beliebiger Abfolge lesen. Gehen Sie unmittelbar weiter zu den Gesten oder springen Sie zu den Filmen. Wenn Sie mehr über Popover erfahren wollen, sollten Sie mit Kapitel 4, Popover und modale Dialoge, beginnen.
Kapitel 3
Gesten nutzen Die größere Oberfläche des iPads ist nicht nur für Ihre Augen gedacht. Sie soll vor allem Ihre Hände ins Spiel bringen und Ihnen ermöglichen, über Gesten mit Ihren Apps zu interagieren. Natürlich nutzt auch das iPhone Multi-Touch als primäres Interaktionsmittel, aber der kleine Bildschirm verhindert, dass man Gesten vollständig ausreizen kann, und schränkt die Möglichkeiten ein, eine überzeugende App-Interaktion zu gestalten. Es gab einen weiteren Aspekt des iPhones, der den Einsatz von Gesten in Apps zu einer Herausforderung machte – das SDK. Wenn Sie Gesten schreiben wollten, mussten Sie Multi-Touch-Code per Copy and Paste zwischen Klassen und Apps austauschen, ohne dass es ein praktisches Mittel zur Wiederverwendung gab. Mit iOS 3.2 hat Apple geändert, wie Gesten in die UIKit-Architektur eingepasst sind, und Ihnen die Möglichkeit gegeben, Gesten auf dem iPad zu abstrahieren und wiederzuverwenden (seit iOS 4.0 sind die gleichen Werkzeuge auch auf dem iPhone verfügbar). Apple bietet außerdem einige Fertiggesten, die Sie nutzen können, einschließlich Tap, Pan, Pinch, Rotation und Swipe – und wenn die eingebauten Gesten Ihren Anforderungen nicht genügen, können Sie die neue Architektur recht einfach erweitern, um eigene zu schreiben. Wie Sie in diesem Kapitel sehen werden, hat Apple die verfügbaren Werkzeuge zur Erstellung deutlich verbessert. Aber denken Sie daran, dass Sie eine ganze Menge wissen müssen, wenn Sie vorgefertigte Recognizer nutzen (oder neue Gesten-Recognizer schreiben) wollen, und in diesem Kapitel werden wir Gesten im Detail untersuchen. Zunächst werden wir vorgefertigte Recognizer nehmen und konfigurieren. Dann werden wir ausgefeiltere Verhalten hinzufügen und anpas-
50 X Kapitel 3: Gesten nutzen sen und damit steuern, wie sich die Recognizer verhalten. Schließlich werden wir einen Recognizer erstellen, der auf unsere eigenen Gesten achtet. Legen wir los!
3.1
iPad Virtual Bubble Wrap Wer hätte 1957, als die Sealed Air Corporation Bubble Wrap™ erfand, gedacht, dass dieses Produkt ein Eigenleben als suchtgefährdende und äußerst amüsante Freizeitbeschäftigung für alle Altersstufen entwickeln würde?1 Denn wer spürt nicht den Drang, die kleinen, mit Luft gefüllten Bläschen zu zerquetschen, wenn ein jungfräulicher Bogen vor ihm auf dem Tisch liegt? Schließlich betrat eine weitere Erfindung die Bühne, das Internet, und brachte uns virtuelle Luftpolsterfolie. Und heute muss man nur noch „Bubble Wrap” sagen, um die Antwort „There is an App for that!“ zu bekommen. Bubble Wrap hat seinen Weg auf das iPhone gefunden. Und was kommt danach? Virtual Bubble Wrap für das iPad natürlich – stellen Sie sich nur diesen gewaltigen Bildschirm vor und Multi-Touch-Gesten, nur dazu gedacht, simulierte Luftbläschen zum Platzen zu bringen. In diesem Kapitel werden wir eine eigene Virtual Bubble Wrap-App schreiben und dabei die neue Gesten-API vollständig erforschen. Statt den üblichen Bogen mit Luftbläschen zu präsentieren, die zum Platzen gebracht werden können, werden wir Gesten nutzen, um die Sache etwas interessanter zu gestalten. In dieser App werden wir eigene Bubble Wrap-Layouts (mit Tap-Gesten) erstellen, alle Bläschen auf einmal zerdrücken (indem die Tap-Geste etwas anders genutzt wird), Bläschen vergrößern (mit einer Pinch-Geste), den Bildschirm zurücksetzen und von vorn beginnen (mit einer Swipe-Geste) sowie eine eigene Geste implementieren, über die Sie Bläschen löschen können. Dazu werden wir zunächst die folgenden Schritte ausführen: 1. Wir erstellen eine einfache View-basierte App, die als „Luftpolsterfolie“ dient. 2. Wir nutzen eine eingebaute Geste, um eine Tap-Geste einzufangen und bei einem Tippen ein Bläschen auf der Folie zu platzieren. 3. Wir nutzen die Tap-Geste erneut, um die Blase zerplatzen zu lassen. 1 Falls Sie nicht wissen, was Bubble Wrap bzw. Luftpolsterfolie ist: Es handelt sich um eine transparente Plastikfolie mit kleinen Luftblasen, die als Verpackungsmaterial zum Schutz von Gegenständen dient. Das Zerquetschen der Luftblasen (und der begleitende Knall) können einen über Stunden erfreuen.
Einfache Tap-Gesten nutzen Wenn wir diese Grundlagen eingerichtet haben, kehren wir zurück und ergänzen Vergrößern, Zurücksetzen und Löschen – alles über Gesten.
Abbildung 3.1: Die Bubble Wrap-Oberfläche
3.2
Einfache Tap-Gesten nutzen Bevor wir loslegen, sollten wir uns kurz Zeit für eine kleine Plauderei nehmen. Wenn Sie bereits Erfahrungen mit UIKit-Views gesammelt haben und auch mit Multi-Touch-Events einigermaßen vertraut sind, dann ist das neue Gestendesign leicht verständlich. Die neue Architektur basiert auf Gesten-Recognizern, die Sie instantiieren und an einen View knüpfen können. Diese Recognizer fungieren als Beobachter für Multi-Touch-Events und erhalten diese vor Ihren Views. Recognizer verarbeiten Multi-Touch-Events, bis die Geste erkannt ist, und senden dann eine Action-Nachricht an das von Ihnen angegebene Ziel. Erkennt Ihr Recognizer in einer Folge von Multi-Touch-Events keine Geste, werden die Events an andere Recognizer weitergeleitet (wenn Sie welche
W
51
52 X Kapitel 3: Gesten nutzen haben) und, wenn so keine Gesten erkannt werden, abschließend an den View selbst. Nachdem wir diese kurze Zusammenfassung hinter uns haben, sollten wir mit der Implementierung beginnen, da man Gesten am besten versteht, wenn man sie im Einsatz sieht. Um die Sache in Gang zu bringen, haben wir ein Projekt namens Bubbles1 erstellt, das Sie im gestures-Verzeichnis des Beispielcodes finden. Um der Anwendung einen Luftpolsterfolien-mäßigeren Anstrich zu geben, haben wir uns die Freiheit genommen, die Bubbles1ViewController.xib-Nib im Interface Builder zu öffnen und dem Hintergrund ein freundliches Mittelgrau zu geben. Zur Erhöhung der Dramatik (die Sie später sehen werden) haben wir in der MainWindow.xib-NibDatei die Hintergrundfarbe des Hauptfensters in Schwarz geändert. Schließlich haben wir in das Projekt zwei Bilder platziert, das einer gefüllten Blase (namens bubble.png) und das einer geplatzten Blase (namens popped.png).
Die Geste instantiieren Unsere erste Aufgabe ist die Erstellung einer Tap-Geste, die, wenn sie erkannt wird, dazu führt, dass eine Blase in den Haupt-View der Anwendung platziert wird. Um das zu erreichen, überschreiben wir in Bubbles1ViewController viewDidLoad und instantiieren einen TapRecognizer. Werfen wir einen Blick darauf: gestures/Bubbles1/Classes/Bubbles1ViewController.m
- (void)viewDidLoad { [super viewDidLoad]; UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapFrom:)]; }
Hier instantiieren wir einen Tap-Recognizer, indem wir die initWithTarget:action:-Methode von UITapGestureRecognizer aufrufen. Gesten-Recognizer nutzen das übliche Cocoa-Target-Action-Muster, bei dem einem Ziel eine Aktionsnachricht gesendet wird, wenn die Geste erkannt wird. Wir haben self als Ziel und handleTapFrom: als Aktion angegeben. Ist die Geste erkannt, wird die Methode handleTapFrom auf unserer Bubbles1ViewController -Instanz aufgerufen.
Einfache Tap-Gesten nutzen
Die Geste an einen View binden Jede Geste müssen Sie nach der Instantiierung über Eigenschaften konfigurieren und an einen View binden, damit Sie sie nutzen können. So machen wir das mit unserem Tap-Recognizer: gestures/Bubbles1/Classes/Bubbles1ViewController.m
-(void)viewDidLoad { [super viewDidLoad]; UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapFrom:)]; [tapRecognizer setNumberOfTapsRequired:1]; [self.view addGestureRecognizer:tapRecognizer]; [tapRecognizer release];
X X X }
Erst konfigurieren wir den Recognizer über seine setNumberOfTapsRequired:-Methode, über die wir angeben können, auf wie viele Taps der Recognizer achten soll. Hier ist ein einfaches Tippen wahrscheinlich die natürlichste Geste für den Anwender. Wie Sie vermutlich erraten haben, ist die Methode setNumberOfTapsRequired: für Tap-Recognizer spezifisch. Wir werden sehen, dass andere Recognizer-Arten ihre eigenen spezifischen Konfigurationseigenschaften haben. Dann nehmen wir den View des Bubbles1ViewController und binden mit der Methode addGestureRecognizer den tapRecognizer an – das ist eine neue Methode, die der UIView-Schnittstelle in iOS 3.2 hinzugefügt wurde. Sobald der Recognizer an den View gebunden ist, kann er alle Multi-Touch-Events überwachen, die der View erhält, um nach Gesten zu suchen. Schließlich geben wir, als brave Bürger, den Recognizer frei, den jetzt der View festhält.
Die Geste aufgreifen Nachdem wir den Tap-Recognizer instantiiert und an einen View gebunden haben, müssen wir das spezifische Verhalten für die Aktion implementieren – und eine Luftblase in den View platzieren. Wie gehen wir das an? Wir werden den Ort des Tippens ermitteln, ein Bild erzeugen, das die Luftblase darstellt, und es dann dem View von Bubbles1ViewController hinzufügen. Schreiben wir also zunächst eine handleTapFrom:-Methode, die den Ort des Tippens ermittelt und eine Luftblase erstellt:
W
53
54 X Kapitel 3: Gesten nutzen gestures/Bubbles1/Classes/Bubbles1ViewController.m
- (void)handleTapFrom:(UITapGestureRecognizer *)recognizer { CGPoint location = [recognizer locationInView:self.view]; CGRect rect = CGRectMake(location.x -40, location.y -40, 80.0f, 80.0f); UIImageView *image = [[UIImageView alloc] initWithFrame:rect]; [image setImage:[UIImage imageNamed:@"bubble.png"]]; [self.view addSubview:image]; [image release]; }
Gehen wir diesen Code einmal durch: handleTapFrom: erhält das recognizer -Objekt, das die Geste akzeptierte.2 Wir können diesen Recognizer nutzen, um den Ort des Tippens zu ermitteln, indem wir seine locationInView:-Methode aufrufen, die uns für den übergebenen View den Punkt liefert, an dem die Geste auftrat. Hier interessieren wir uns hauptsächlich für den Ort im View des Bubbles1ViewController, übergeben also self.view als View. Nachdem wir den Punkt dann haben, der angetippt wurde, müssen wir eine Luftblase erzeugen und an diesen Punkt setzen. Dazu berechnen wir ein Rahmenrechteck für das Bild (indem wir uns auf den Punkt des Taps beziehen), instantiieren ein Bild in diesem Rahmenrechteck und setzen die Quelle des Bildes auf bubbles.png. Schließlich müssen wir das Bild nur noch als Subview zu self.view hinzufügen.
Einige Luftbläschen schaffen Jetzt sind Sie so weit, dass Sie den Code testen können. Erstellen Sie das Projekt und führen es aus, sollten Sie einen leeren, grauen Bildschirm sehen. Tippen Sie nun einfach in den View (oder klicken Sie, wenn Sie den Simulator nutzen), und Sie erkennen an den entsprechenden Punkten Luftblasen. Fassen wir, bevor wir fortschreiten, kurz zusammen, was wir gemacht haben: 앫
Wir haben einen Tap-Gesten-Recognizer erstellt und ihn so konfiguriert, dass er Ein-Finger-Tipp-Gesten erkennt.
2 Was es heißt, eine Geste zu akzeptieren, werden wir uns weiter unten in diesem Kapitel ansehen. Vorweg: Sieht ein Tap-Recognizer einen Multi-Touch-Event-Satz, der nach einer Tap-Geste aussieht, erkennt er die Geste und ruft die Aktion auf dem Ziel auf.
Multi-Touch-Events und die View-Hierarchie W 55 앫
Wir haben dieses Gesten-Objekt an den View unseres Bubbles1ViewController gebunden, damit unser Gesten-Recognizer bei jedem Multi-Touch-Event auf dem View einen Blick auf das Event werfen darf und entscheiden kann, ob die Geste eine Tap-Geste darstellt.
앫
Wenn es sich um eine Tap-Geste handelt, soll der Gesten-Recognizer die handleTapFrom:-Methode auf unserem Bubbles1ViewController aufrufen.
앫
Dann haben wir den Code für die Methode handleTapFrom: geschrieben, die den Ort des Tippens über das Recognizer-Objekt ermittelt, ein Bild erstellt, das wie eine Luftblase aussieht, und dieses dann am ermittelten Ort anzeigt.
Nicht schlecht für die paar Zeilen Code, nicht wahr? Aber wir wollen die Sache noch etwas interessanter gestalten, indem wir den Luftblasen die Möglichkeit zu platzen verleihen.
3.3
Multi-Touch-Events und die View-Hierarchie Wenn wir Luftblasen platzen lassen wollen, müssen davon erfahren, wenn der Benutzer auf eine Blase tippt. Wie bewerkstelligen wir das? Vielleicht erwarten Sie, dass man erkennen kann, wann eine Luftblase angetippt wird (oder in unserem Fall ein UIImageView, der eine Luftblase darstellt), indem man an jeden UIImageView eine weitere TapGeste bindet und eine weitere Aktion schreibt, die sich um den Rest kümmert. Das würde funktionieren, und wir werden dieses Verfahren später einsetzen, um eine Geste zu implementieren, über die die Größe geändert wird, indem wir eine Geste unmittelbar an einen Image-View binden. Hier aber werden wir ein anderes, gradlinigeres Verfahren nutzen, um zu zeigen, wie die View-Hierarchie mit Multi-Touch arbeitet. Die folgenden Abschnitte sind wichtig, weil sie detailliert ausführen, wie Multi-Touch-Events in der View-Hierarchie funktionieren und wie sie sich auf Gesten beziehen. Schauen wir uns die Details an und bringen wir dann diese Blasen zum Platzen. Empfängt das iPad ein Multi-Touch-Event, wird es standardmäßig von der Anwendung an das Fenster, an den View des Fensters und von diesem zu allen Subviews weitergereicht, in deren Grenzen die Berührung fällt. Den letzten View in der Hierarchie, in den die Berührung fällt, bezeichnet man als Hit-Tested-View. Wenn Gesten erkannt werden sollen, darf der Hit-Tested-View als Erster damit beginnen, Multi-TouchEvents zu erkennen. Erkennt er keine, reicht er sie in der Kette wieder
56 X Kapitel 3: Gesten nutzen nach oben an die Eltern-Views (oder in letzter Instanz an das Fenster und die Anwendung, wenn kein View die Events nutzt). In unserem aktuellen Code darf zuerst der UIImageView einer Luftblase versuchen, in einer Berührung eine Geste zu erkennen. Erkennt er keine Geste, gehen die Events an den Eltern-View, hier den View des Bubbles1ViewController. Weil wir noch keine Geste bzw. keinerlei Code zur Erkennung von Multi-Touch-Events im UIImageView geschrieben haben, steigen alle Events zum View des Bubbles1ViewController auf. Dort werden Taps erkannt, auch wenn wir auf eins der Luftblasenbilder tippen. Das alles klingt etwas kompliziert. Warum installieren wir nicht einfach eine weitere Geste auf dem UIImageView, um damit durch zu sein? Ganz einfach: Wenn wir unser neues Wissen zur View-Hierarchie und zur Trefferprüfung anwenden, können wir erheblich einfacheren Code schreiben, um das Platzen der Luftbläschen zu implementieren. Und Ihnen hilft es später, auf Basis Ihres Verständnisses von Multi-TouchEvents Gesten auf ausgefeiltere Weise zu nutzen (auch in diesem Kapitel werden wir es noch einmal zum Einsatz bringen, wenn wir unsere eigene Geste schreiben).
Das Platzen implementieren Nachdem Sie jetzt eine solide Basis haben, können wir endlich damit beginnen, dieses Wissen auch anzuwenden. Anhand folgender Schritte werden wir den Code zum Platzen einer Luftblase schreiben: Wir werden die bereits vorhandene Tap-Geste nutzen, die wir schon an den View des Bubbles1ViewController geknüpft haben. Tippt der Benutzer auf eine Luftblase, wird das Touch-Event erst an den UIImageView der Luftblase gegeben, der aber keine Geste kennt, mit der er es verarbeiten kann – es steigt also zum View des Bubbles1ViewController auf, in dem unsere vorhandene Geste das Tippen erkennt. Wird an diesem Punkt die Methode handleTapFrom: aufgerufen, werden wir (indem wir uns den Hit-Tested-View ansehen) ermitteln, auf welchen View getippt wurde. War es einer der UIImageViews, ändern wir dessen Quellbild in popped.png, wie wir hier dargestellt haben: Tap-Geste auf eine Luftblase
Luftblase
Geplatzte Luftblase
Multi-Touch-Events und die View-Hierarchie W 57 Schauen wir uns den Code an. Sie werden sehen, dass wir der Aktion handleTapFrom: einige Dinge hinzugefügt haben: gestures/Bubbles2/Classes/Bubbles2ViewController.m
- (void)handleTapFrom:(UITapGestureRecognizer *)recognizer { CGPoint location = [recognizer locationInView:self.view]; UIView *hitView = [self.view hitTest:location withEvent:nil]; if ([hitView isKindOfClass:[UIImageView class]]) {
X X X X X X
[(UIImageView *)hitView setImage:[UIImage imageNamed:@"popped.png"]]; } else { CGRect rect = CGRectMake(location.x -40, location.y -40, 80.0f, 80.0f); UIImageView *image = [[UIImageView alloc] initWithFrame:rect]; [image setImage:[UIImage imageNamed:@"bubble.png"]]; [image setUserInteractionEnabled: YES]; [self.view addSubview:image]; [image release]; } }
Rufen Sie sich aus der letzten Version dieser Methode ins Gedächtnis, dass wir erst den Ort des Taps aus dem Recognizer abrufen müssen. Unser neuer Code nimmt diesen Ort und ruft hitTest:withEvent: auf, um den Hit-Tested-View abzurufen. Handelt es sich um einen Tap auf einen Blase, erfolgte er auf einem UIImageView, andernfalls auf dem View des Bubbles1ViewController. Unter diesen Voraussetzungen müssen wir also nur prüfen, ob der HitTested-View ein UIImageView ist, und, wenn das der Fall ist, das Bild von bubbles.png in popped.png ändern. Ist der Hit-Tested-View kein Image-View, muss es der View unseres Bubbles1ViewController sein, was bedeutet, dass der Benutzer eine weitere Blase auf dem View entstehen lassen will. In dem Fall nutzen wir also den Code, den wir zuvor geschrieben hatten. Beachten Sie, dass wir den alten Code um einen Aspekt ergänzt haben, um ein Problem mit UIImageViews zu bewältigen. Es zeigt sich, dass Image-Views so konfiguriert sind, dass sie Multi-Touch-Events standardmäßig ignorieren. Deswegen setzen wir setUserInteractionEnabled auf YES, um eine Interaktion mit Image-Views über Touch-Events zu ermöglichen. Hätten wir setUserInteractionEnabled nicht auf YES gesetzt, hätten wir das Multi-Touch-Event im View des Controllers erhalten. Die Image-Views wären nie zum Hit-Tested-View geworden (und wir hätten nie Blasen zum Platzen bringen können).
58 X Kapitel 3: Gesten nutzen
Die Luftpolsterfolie testen Jetzt sind Sie bereit, die vollständig funktionierende Luftpolsterfolie zu erstellen und auszuführen. Tippen Sie auf die leeren Bereiche des View, um neue Luftblasen zu erstellen, und klicken Sie dann auf die Blasen, um sie zum Platzen zu bringen. Sieht man von einem saftigen Platzgeräusch ab (das wir Ihnen als Übung überlassen), ist diese Implementierung schon eine recht nette virtuelle Luftpolsterfolie (und wurde mit sehr wenig Code erzeugt!).
3.4
UIGestureRecognizer und die Swipe-Geste Wie bereits angesprochen, gibt es neben der Tap-Geste noch weitere Gesten. Der UITapGestureRecognizer ist gar Teil einer umfangreichen Familie von Recognizern, die alle Unterklassen von UIGestureRecognizer sind, einer abstrakten Klasse, die die grundlegende Funktionalität und Schnittstelle für Gesten-Recognizer definiert. Die Details dieser abstrakten Klasse werden wir uns etwas später in diesem Kapitel ansehen, zunächst aber werden wir unseren Blick einer weiteren Unterklasse zuwenden: UISwipeGestureRecognizer.
Eine Swipe-Geste erkennen Die Swipe-Geste, also ein Wischen, werden wir nutzen, um die Folie zu löschen und uns eine blitzblanke neue Oberfläche zu schaffen, auf der wir neue Blasen herstellen können. Anders gesagt: Wischt Ihr Finger über den Bildschirm, werden alle Blasen gelöscht, sodass Sie wieder eine leere Folie haben. Der Code für das Erkennen eines Wischens ähnelt dem für das Erkennen des Tippens, die Swipe-Geste hat jedoch einige spezifische Elemente. Beginnen wir damit, dass wir den SwipeRecognizer instantiieren und konfigurieren: gestures/Bubbles3/Classes/Bubbles3ViewController.m
- (void)viewDidLoad { [super viewDidLoad]; // Hier steht unser alter Code. X X X X X X
UISwipeGestureRecognizer *swipeRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipeFrom:)]; swipeRecognizer.direction = UISwipeGestureRecognizerDirectionRight | UISwipeGestureRecognizerDirectionLeft;
UIGestureRecognizer und die Swipe-Geste [self.view addGestureRecognizer:swipeRecognizer]; [swipeRecognizer release];
X X }
Wirkt das vertraut? Gehen wir den Code schrittweise durch: Zuerst reservieren und initialisieren wir ein UISwipeGestureRecognizer -Objekt mit einem Ziel und einer Aktion – genau wie wir es beim Tap-Recognizer gemacht haben. Hier werden wir eine Aktion namens handleSwipeFrom: schreiben, die die Swipe-Geste verarbeitet, wenn sie erkannt wird. Dann bestimmen wir die Wischrichtung – uns stehen vier zur Auswahl: 앫 UISwipeGestureRecognizerDirectionLeft 앫 UISwipeGestureRecognizerDirectionRight 앫 UISwipeGestureRecognizerDirectionUp 앫 UISwipeGestureRecognizerDirectionDown
Wir haben zwei Richtungen (links und rechts) angegeben, die über eine bitweise ODER-Operation verknüpft werden müssen. Aber Sie könnten eigentlich jede beliebige Kombination von Richtungen wählen. Nachdem wir den Swipe-Recognizer konfiguriert haben, fügen wir den Recognizer dem View des Controllers hinzu – ebenfalls genau wie beim Tap-Recognizer. Jetzt müssen wir die handleSwipeFrom-Aktion schreiben, die sich darum kümmert, dass unsere virtuelle Luftpolsterfolie geleert wird.
Die Luftpolsterfolie leeren Ein schematisches Leeren der Luftpolsterfolie ist eine einfache Aufgabe. Wir müssen alle Subviews des Bubbles1ViewController ausfindig machen und entfernen. Für den Fall, dass Sie noch nicht mit Subviews gearbeitet haben: Die UIView-Methode subviews: liefert ein Array mit allen Subviews. Dieses Array durchlaufen wir und rufen dabei für jeden Subview removeFromSuperview: auf. Die Methode removeFromSuperview: entfernt den View aus seinem Eltern-View (hier der View des Bubbles1ViewController). Setzen wir das um: gestures/Bubbles3/Classes/Bubbles3ViewController.m
- (void)handleSwipeFrom:(UISwipeGestureRecognizer *)recognizer { for (UIView *subview in [self.view subviews]) { [subview removeFromSuperview]; }
W
59
60 X Kapitel 3: Gesten nutzen [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:.75]; [UIView setAnimationBeginsFromCurrentState:YES]; [UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:self.view cache:YES]; [UIView commitAnimations]; }
Zusätzlich haben wir uns dazu entschieden, eine kleine Animation einzusetzen, die anzeigt, dass das Wischen eine Aktion angestoßen hat. Obwohl Animationen nicht Gegenstand dieses Kapitels sind, wollen wir kurz erläutern, was wir hier tun: Wir richten eine einfache Flip-Animation ein, die über eine Dauer von 0,75 Sekunden ausgeführt wird. Dabei wird einfach der gleiche View gewendet. Sind die Subviews gelöscht, soll die Animation zeigen, wie ein leerer View gewendet wird. Mehr zur Erstellung von Animationen finden Sie in einem Buch, das wir weiter oben in diesem Kapitel empfohlen haben: Core Animation for Mac OS X and the iPhone: Creating Compelling Dynamic User Interfaces [Dud08]. Für die Swipe-Geste war das schon alles – kompilieren Sie nun. Wischen Sie mit einem Finger nach links oder rechts über den Bildschirm und schauen Sie sich an, wie eine neue Luftpolsterfolie umschlägt, bereit für noch etwas Plop-Spaß (siehe Abbildung 3.2).
Abbildung 3.2: Eine animierte Swipe-Geste
Diskrete und kontinuierliche Gesten W 61
Mehrere Finger einsetzen Die Klasse UISwipeGestureRecognizer hat außerdem eine numberOfTouchesRequired-Eigenschaft, die auf die Anzahl von Berührungen (oder Finger) gesetzt werden kann, die für die Swipe-Geste erforderlich sind. Setzen Sie diese Eigenschaft auf 2, erkennt der Swipe-Recognizer die Geste beispielsweise nur, wenn beim Wischen zwei Finger eingesetzt werden. 3 Der UITapGestureRecognizer hat ebenfalls eine numberOfTouchesRequired-Eigenschaft. Auch wenn Tap-Gesten mit mehreren Fingern nicht ganz so verbreitet sind wie Swipe-Gesten mit mehreren Fingern, können Sie sogar Tap-Recognizer schreiben, die mehrere Berührungen verlangen.
3.5
Diskrete und kontinuierliche Gesten Bislang haben wir uns zwei Gesten-Recognizer angesehen: Tap und Swipe. Beide senden eine einzige Aktionsnachricht an das Ziel, wenn die entsprechende Geste erkannt wird – derartige Gesten nennen wir diskrete Gesten. Es gibt noch eine weitere Art von Recognizer, die auf Basis einer einzelnen Geste mehrere Aktionsnachrichten sendet und als kontinuierliche Geste bezeichnet wird. Denken Sie beispielsweise an die Pinch-Geste auf Ihrem iPhone (oder iPad). Das Resultat auf dem Bildschirm folgt dem Verlauf der Geste. Die Größe des Bilds ändert sich, während Sie Ihre Finger zusammenführen, bis Sie die Finger vom Bildschirm nehmen.
3 Beachten Sie, dass Sie die Gesten auf dem Gerät testen müssen, wenn Sie hier einen Wert größer 1 einsetzen, da der Simulator keine Möglichkeit bietet, Berührungen mit mehreren Fingern zu simulieren (etwas weiter unten werden Sie allerdings sehen, dass es immerhin eine Möglichkeit gibt, Pinch-Gesten zu testen).
62 X Kapitel 3: Gesten nutzen
Die Bläschengröße mit einer Pinch-Geste ändern Erstellen wir eine Pinch-Geste, d.h. ein Spreizen bzw. Stauchen, die uns gestattet, die Größe der Bläschen zu ändern (sie entweder größer oder kleiner zu machen) – Abbildung 3.3 zeigt, wie das aussieht. Um die Größenveränderung zu ermöglichen, binden wir die Pinch-Geste unmittelbar an die einzelnen UIImageViews – wie das funktionieren könnte, haben wir bereits für Tap-Gesten auf Luftblasenbildern besprochen. Jetzt wollen wir versuchen, das bei der Pinch-Geste umzusetzen. Wir werden die Pinch-Geste nicht in der Methode viewDidLoad instantiieren, sondern unseren Code in die Methode handleTapFrom: stecken. Die Bilder der Luftblasen werden in der Methode handleTapFrom: erstellt; sie ist also der geeignetste Ort, um die Geste an die Bilder zu binden.
Abbildung 3.3: Veränderte UIIMAGES in der Luftpolsterfolie
Diskrete und kontinuierliche Gesten W 63 Schauen wir uns den Code zur Erstellung der UIPinchGestureRecognizer -Objekte an: gestures/Bubbles4/Classes/Bubbles4ViewController.m
- (void)handleTapFrom:(UITapGestureRecognizer *)recognizer { CGPoint location = [recognizer locationInView:self.view]; UIView *hitView = [self.view hitTest:location withEvent:nil]; if ([hitView isKindOfClass:[UIImageView class]]) { [(UIImageView *)hitView setImage:[UIImage imageNamed:@"popped.png"]]; } else { CGRect rect = CGRectMake(location.x -40, location.y -40, 80.0f, 80.0f); UIImageView *image = [[UIImageView alloc] initWithFrame:rect]; [image setImage:[UIImage imageNamed:@"bubble.png"]]; [image setUserInteractionEnabled: YES]; UIPinchGestureRecognizer *pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchFrom:)]; [image addGestureRecognizer:pinchRecognizer]; [pinchRecognizer release];
X X X X X X
[self.view addSubview:image]; [image release]; } }
Auch dieser Code sollte vertraut wirken; wir instantiieren den UIPinchGestureRecognizer, setzen seine Aktion auf handlePinchFrom: und fügen die Geste den einzelnen Image-Views hinzu. Wir nutzen diesen Recognizer so, wie er ist, ohne eine spezielle Konfiguration. Nachdem wir die Geste an einen View gebunden haben, können wir uns der interessanteren Aufgabe widmen: den Code zu schreiben, der die Aktionsnachrichten des Pinch-Recognizers verarbeitet und auf dem Bildschirm die Größe der Luftblasen der Geste entsprechend anpasst. Dazu nutzen wir die scale-Eigenschaft des UIPinchGestureRecognizer, die den Skalierungsfaktor der Geste darstellt. Das funktioniert folgendermaßen: Sobald die Pinch-Geste beginnt, wird der Skalierungsfaktor geändert, während Sie Ihre Finger spreizen oder zusammenführen, und kontinuierlich an Ihre Aktionsmethode geliefert. Wenn Sie den Skalierungsfaktor haben, müssen Sie nur noch die
64 X Kapitel 3: Gesten nutzen Größe des Luftblasenbilds dem Skalierungsfaktor entsprechend anpassen. Schauen wir uns genauer an, wie man das macht: gestures/Bubbles4/Classes/Bubbles4ViewController.m
-(void)handlePinchFrom:(UIPinchGestureRecognizer *)recognizer { CGFloat scale = [recognizer scale]; CGAffineTransform transform = CGAffineTransformMakeScale(scale, scale); recognizer.view.transform = transform; }
Erst nutzen wir den Recognizer und rufen seine scale-Eigenschaft ab, die einen Wert enthält, der die Skalierung repräsentiert, die die PinchGeste anzeigt. Dann verwenden wir scale, um eine Transformationsmatrix zu erstellen. Sollten Sie mit Transformationen nicht vertraut sein: Sie können eingesetzt werden, um die Koordinaten eines View zu manipulieren, und bieten Ihnen ein natürliches Mittel, Views zu skalieren oder zu drehen – wenn Sie die transform-Eigenschaft des View auf eine Transformationsmatrix setzen, wird der View gemäß der Skalierung in der Matrix gezeichnet. Hier weisen wir die Transformation dem View zu, der mit dem Recognizer verknüpft ist, d.h. einem unserer Image-Views. Nachdem ihm die Transformation zugewiesen wurde, wird der View neu gezeichnet und den Werten in der Matrix entsprechend skaliert. Transformationen sind ein umfangreiches Thema, das den Horizont dieses Buchs weit übersteigt. Mehr erfahren Sie in Core Animation for Mac OS X and the iPhone: Creating Compelling Dynamic User Interfaces [Dud08].
Das Spreizen testen Mehr Code brauchen wir nicht; erstellen Sie die App und führen Sie den neuen Code aus. Erzeugen Sie einige Luftblasen und ziehen Sie dann Ihre Finger auseinander – Sie werden sehen, wie dabei die Luftblasen wachsen. Gleichermaßen können Sie Ihre Finger wieder zusammenführen und die Luftblasen damit schrumpfen. Wenn Sie den Simulator nutzen, können Sie das Spreizen simulieren, indem Sie die Optionstaste gedrückt halten (es müssten zwei Punkte zu sehen sein, die Ihre Finger repräsentieren). Positionieren Sie die beiden Punkte mit der Maus (oder dem Trackpad) und klicken Sie dann und bewegen Sie die Maus, um das Spreizen und Zusammenführen Ihrer Finger zu simulieren.
Eigene Gesten erstellen W 65
3.6
Eigene Gesten erstellen Apple bietet einige ausgezeichnete Gesten an, aber was ist, wenn diese Ihren Anforderungen nicht genügen? Dann können Sie natürlich eigene Gesten erstellen. Apple hat diesen Prozess geradlinig gestaltet, indem die Erweiterung der Basisklasse zur Gestenerkennung mit eigenen Klassen gestattet wird. Dennoch der Hinweis: Das Schreiben eigener Gesten erfordert, dass Sie etwas genauer wissen, wie Gesten funktionieren, und genau das werden wir uns in diesem Abschnitt ansehen. Dann werden wir dieses Wissen einsetzen, um eine eigene Geste für unsere Luftpolsterfolienanwendung zu schreiben.
Der Recognizer kann keine Geste erkennen
Der Recognizer erkennt eine Geste
Abbildung 3.4: Die Zustände eines diskreten Gesten-Recognizers
Wie Gesten-Recognizer tatsächlich funktionieren Betrachten Sie Gesten-Recognizer als einfache Zustandsmaschinen, die von einem Zustand in den anderen übergehen, je nachdem, welche Touch-Events sie empfangen, während der Benutzer mit dem Bildschirm interagiert. Alle Recognizer beginnen im gleichen Zustand: dem UIGestureRecognizerStatePossible-Zustand, der anzeigt, dass der Recognizer Touch-Events untersucht, um Gesten aufzuspüren. Ist eine Geste erkannt, gehen diskrete Gesten in den UIGestureRecognizerStateRecognized-Zustand über. Findet der Recognizer hingegen keine Geste, wechselt er in den Zustand UIGestureRecognizerStateFailed. Bei kontinuierlichen Gesten läuft das etwas anders. Wie diskrete Gesten beginnen auch sie mit dem Zustand UIGestureRecognizerStatePossible. Aber wenn der Anfang einer kontinuierlichen Geste erkannt wird, wechselt der Recognizer in den Zustand UIGestureRecognizer StateBegan. Während sich die Geste dann mit der Zeit ändert, wechselt der Recognizer in den Zustand UIGestureRecognizerStateChanged. Das heißt, solange sich die Geste weiterhin ändert, setzt der Recognizer sei-
66 X Kapitel 3: Gesten nutzen nen Zustand auf UIGestureRecognizerStateChanged. Aus diesem Zustand heraus kann der Recognizer zwei Wege einschlagen: Wenn der Anwender seine Finger hebt, wechselt der Zustand zu UIGestureRecognizerStateEnded; der Recognizer kann jedoch auch entscheiden, dass die Geste nicht länger erfüllt wird, und in den Zustand UIGestureRecognizerStateCancelled wechseln.
Der Recognizer erkennt eine Geste
Der Recognizer kann keine Geste erkennen
Die Geste ändert sich
Der letzte Finger in einer Geste wird entfernt
Recognizer oder System brechen die Geste ab
Abbildung 3.5: Zustände eines kontinuierlichen Gesten-Recognizers Veranschaulichen wir uns die unterschiedlichen Arten von Recognizern an konkreteren Beispielen. Beginnen wir mit einem diskreten Recognizer wie dem Ein-Finger-Tap-Recognizer. Dieser Recognizer befindet sich zu Anfang im Zustand UIGestureRecognizerStatePossible und wechselt, sobald ein Touch-Event auftritt, das ein Tap ist, in den Zustand UIGestureRecognizerStateRecognized. Nehmen Sie nun dagegen den kontinuierlichen Recognizer für die Pinch-Geste. Hier wechselt
Eigene Gesten erstellen W 67 der Recognizer in den UIGestureRecognizerStateBegan-Zustand, wenn zwei Finger auf der Oberfläche platziert werden, und setzt dann wiederholt den Zustand auf UIGestureRecognizerStateChanged, während die Finger die Pinch-Geste vollziehen. Entfernt der Benutzer seine Finger, setzt der Recognizer den Zustand UIGestureRecognizerStateEnded.
Eine Löschen-Geste erstellen Nachdem wir uns das eingeprägt haben, können wir über die Geste nachdenken, die wir implementieren werden: Wenn Sie je einen Apple Newton in den Fingern hatten, werden Sie sich wahrscheinlich an eine Geste erinnern, bei der Sie eine Zickzacklinie über einem Objekt zeichnen mussten, die der Bewegung ähnelt, die man mit einem Radiergummi macht. Wurde diese Geste erkannt, löschte der Newton das darunterliegende Objekt.
Wir werden einen Recognizer implementieren, der eben diese Geste erkennt. Diesen werden wir dann nutzen, um die darunterliegende Luftblase zu löschen. Überlegen Sie einen Augenblick, wie Sie den Code schreiben würden, der diese Geste entdeckt. Obgleich es viele Möglichkeiten gibt, Multi-Touch-Events zu analysieren, um eine radiergummiartige Bewegung zu erkennen, werden wir einfach schauen, was wir mit einer einfachen Heuristik erreichen können. So wird unsere Heuristik funktionieren: Beachten Sie, dass die Löschen-Geste eigentlich nur eine Hoch-runter-Bewegung ist, die wir nachverfolgen können, indem wir einfach die Bewegung entlang der
68 X Kapitel 3: Gesten nutzen y-Achse überwachen. Gibt es im y-Wert einen Wechsel von zunehmend zu abnehmend (oder umgekehrt), kennen wir die Richtung, in die sich der Verlauf der Bewegung geändert hat. Und wenn wir diese Wechsel zählen, können wir nach zwei oder drei Änderungen entscheiden, dass das eine Löschen-Geste ist. Natürlich ist dieses Verfahren nicht perfekt – deswegen bezeichnen wir es auch als heuristisch. Was schiefgehen könnte? Beispielsweise könnte ein Anwender eine lange Wischbewegung machen, die etwas zu sehr abschweift und das Wischen wie ein Löschen aussehen lässt. Oder je nach Wischalgorithmus (den wir nicht kennen) könnte ein zu ausgreifendes Löschen als Wischen interpretiert werden. Wie groß dieses Problem ist? Nicht sehr groß, wie Sie sehen werden, aber dann und wann könnte es auftreten, da die Gesten eine gewissen Ähnlichkeit haben. Aber das ist nicht schlimm, wir werden die Sache weiter unten in diesem Kapitel beheben.
UIGestureRecognizer erweitern Es ist Zeit, dass wir damit beginnen, diesen Recognizer zu schreiben. Das tun wir, indem wir zunächst eine Unterklasse der RecognizerBasisklasse erstellen. Schreiben wir zunächst eine Schnittstellendatei namens DeleteGestureRecognizer.h. gestures/Bubbles5/Classes/DeleteGestureRecognizer.h
#import #import @interface DeleteGestureRecognizer : UIGestureRecognizer { } @end
Hier importieren wir die Header-Datei UIGestureRecognizerSubclass.h, die die UIGestureRecognizer -Basisklasse definiert, und deklarieren eine neue Schnittstelle für DeleteGestureRecognizer, die UIGestureRecognizer erweitert. Jetzt werden wir die Methoden ergänzen, die wir benötigen, um UIGestureRecognizer zu erweitern. Fügen Sie Ihrer Schnittstellendefi-
nition die folgenden Methoden hinzu: gestures/Bubbles5/Classes/DeleteGestureRecognizer.h
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; -(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; -(void)reset;
Eigene Gesten erstellen W 69 Auf diese Methoden werden wir später zurückkommen, um sie vollständig zu beschreiben und in der Implementierungsdatei zu implementieren. Aber bevor wir das tun, werden wir der Schnittstellendatei noch einige weitere Dinge hinzufügen: gestures/Bubbles5/Classes/DeleteGestureRecognizer.h
#import #import
X X X
@interface DeleteGestureRecognizer : UIGestureRecognizer { bool strokeMovingUp; int touchChangedDirection; UIView *viewToDelete; }
X @property (nonatomic, retain) UIView *viewToDelete; -
(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; (void)reset;
@end
Wir haben drei Dinge hinzugefügt: zunächst eine bool-Variable namens strokeMovingUp, die den Wert YES erhält, wenn sich die Geste in positive y-Richtung bewegt (andernfalls NO), dann einen int namens touchChangedDirection, der festhält, wie oft die Geste die Richtung geändert hat, und schließlich einen UIView namens viewToDelete, der eine Referenz auf den View aufnehmen wird, der gelöscht werden soll, wenn die Geste erkannt wird.
Den DeleteGestureRecognizer implementieren Jetzt sollten wir uns an die Implementierung machen. Bauen wir zunächst das Gerüst der Datei DeleteGestureRecognizer auf: gestures/Bubbles5/Classes/DeleteGestureRecognizer.m
@implementation DeleteGestureRecognizer @synthesize viewToDelete; @end
Wir werden nun die Methoden aus der Klasse UIGestureRecognizer durchgehen, die wir überschreiben. Hier sind das die Methode reset sowie alle Methoden aus der Schnittstellendatei, die mit dem Namen touches beginnen. Diese Touch-Methoden sind eine recht enge Entspre-
70 X Kapitel 3: Gesten nutzen chung des Multi-Touch-Event-Handling-Frameworks, das das UIKit unterstützt. Wenn Sie sich dafür interessieren, wie diese Events auf einer Ebene noch unter den Gesten verarbeitet werden, sollten Sie einen Blick in den Abschnitt “Handling Multi-Touch Events” im Event Handling Guide for iOS [App10b] werfen. Wir werden ihre Rolle im UIGestureRecognizer bei der nachfolgenden Implementierung beschreiben. Beginnen wir mit der Implementierung der Methode reset. Diese Methode wird aufgerufen, um den Recognizer zurückzusetzen, wenn er fertig ist – entweder weil er eine Geste erkannt hat oder weil er keine Geste erkannt hat. In beiden Fällen räumt reset den Dreck weg und bereitet die Eigenschaften für den nächsten Versuch vor, indem sie auf ihre Anfangswerte zurückgesetzt werden: gestures/Bubbles5/Classes/DeleteGestureRecognizer.m
- (void)reset { [super reset]; strokeMovingUp = YES; touchChangedDirection = 0; self.viewToDelete = nil; }
Jetzt wird es Zeit für die verschiedenen touches-Methoden. Schauen wir uns diese als Gruppe an, damit wir ihre Funktion verstanden haben, bevor wir uns an die Implementierung machen: 앫
Die Methode touchesBegan:withEvent: wird aufgerufen, wenn einer oder mehrere Finger den View berühren, an den die Geste gebunden ist.
앫
Die Methode touchesMoved:withEvent: wird aufgerufen, wenn diese Finger anfangen, sich zu bewegen.
앫
Die Methode touchesEnded:withEvent: wird aufgerufen, wenn einer oder mehrere Finger vom Bildschirm gehoben werden.
앫
Die Methode touchesCancelled:withEvent: wird aufgerufen, wenn das System beschließt, eine Folge von Events abzubrechen (beispielsweise wenn ein Anruf eingeht).
Unsere Aufgabe besteht nun darin, alle diese Methoden zu gestalten, das erforderliche Verhalten anzubieten und bei Bedarf den Zustand der Geste anzupassen. Unsere Oberklasse, die Klasse UIGestureRecognizer, kümmert sich um die ganzen Kleinigkeiten, wie den Aufruf von reset, das Senden der Aktionsnachrichten an die Ziele der Gesten, und erledigt sämtliche Wartungsaufgaben, die sonst noch im Hintergrund erfolgen.
Eigene Gesten erstellen W 71 Beginnen wir mit der Methode touchesBegan:withEvent:. Das ist die Methode, die aufgerufen wird, wenn die erste Berührung des View erfolgt, an den die Geste gebunden ist. Hier werden wir nur ein einziges Verhalten ergänzen: Wir werden sichern, dass die touchesBegan:withEvent:-Methode der Oberklasse aufgerufen wird und dass die Geste nur eine Berührung (d.h. einen Finger) umfasst. Gibt es mehr Berührungen, lassen wir die Erkennung sofort fehlschlagen, indem wir die Eigenschaft state auf UIGestureRecognizerStateFailed setzen. Das war es schon. Noch ist die Sache gar nicht so kompliziert. gestures/Bubbles5/Classes/DeleteGestureRecognizer.m
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesBegan:touches withEvent:event]; if ([touches count] != 1) { self.state = UIGestureRecognizerStateFailed; return; } }
Unsere nächste Methode ist touchesMoved:withEvent:, die aufgerufen wird, wenn die Geste bewegt wird (anders gesagt, wenn die Finger den View berührt haben und dann bewegt werden, ohne vom Bildschirm genommen zu werden). Dieser Teil des Codes wird etwas interessanter werden. Werfen wir einen Blick darauf: gestures/Bubbles5/Classes/DeleteGestureRecognizer.m
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesMoved:touches withEvent:event]; if (self.state == UIGestureRecognizerStateFailed) return; CGPoint nowPoint = [[touches anyObject] locationInView:self.view]; CGPoint prevPoint = [[touches anyObject] previousLocationInView:self.view]; if (strokeMovingUp == YES) { if (nowPoint.y < prevPoint.y ) { strokeMovingUp = NO; touchChangedDirection++; } } else if (nowPoint.y > prevPoint.y ) { strokeMovingUp = YES; touchChangedDirection++; } }
Wie in allen Touch-Methoden müssen wir zunächst die Oberklasse auf den Plan treten lassen. Wir stellen sicher, dass die Geste nicht schon
72 X Kapitel 3: Gesten nutzen fehlgeschlagen ist, und kehren zurück, sollte das der Fall sein. Dann nutzen wir das Touch-Objekt, um zwei Werte abzurufen: den aktuellen Ort und den vorhergehenden Ort der Bewegung. Mit diesen beiden Orten können wir über einen einfachen Vergleich prüfen, ob es eine Änderung in Richtung der Geste gab. Hier prüfen wir zwei Fälle: Verläuft die Geste nach oben und kehrt dann um, oder verläuft sie nach unten und geht dann wieder nach oben? Trifft eins davon zu, ändern wir unsere Eigenschaft zur Anzeige der Richtung, strokeMovingUp, und erhöhen den Zähler für die Richtungsänderungen in touchChangedDirection. Dann folgt die Methode touchesEnded:withEvent:, die aufgerufen wird, wenn die Finger des Benutzers angehoben werden. Hier ist unsere Aufgabe einfach: Sagt uns die Eigenschaft touchChangedDirection, dass die Richtung drei Mal oder häufiger geändert wurde, erkennen wir die Geste, indem wir die Eigenschaft state auf UIGestureRecognizerStateRecognized setzen. Ist das nicht der Fall, bleibt uns nur noch eine Möglichkeit: den Zustand auf fehlgeschlagen zu setzen. gestures/Bubbles5/Classes/DeleteGestureRecognizer.m
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesEnded:touches withEvent:event]; if (self.state == UIGestureRecognizerStatePossible) { if (touchChangedDirection >= 3) { self.state = UIGestureRecognizerStateRecognized; } else { self.state = UIGestureRecognizerStateFailed; } } }
Schließlich müssen wir uns um touchesCancelled:withEvent: kümmern, das aufgerufen wird, wenn das System das Touch-Event abbrechen will. Wenn das passiert, können wir nur die state-Eigenschaft der Geste auf UIGestureRecognizerStateFailed setzen gestures/Bubbles5/Classes/DeleteGestureRecognizer.m
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesCancelled:touches withEvent:event]; [self reset]; self.state = UIGestureRecognizerStateFailed; }
Glückwunsch! Sie haben gerade einen vollständigen Gesten-Recognizer für das iPad geschrieben. Nehmen Sie sich die Zeit, Ihre Arbeit zu bewundern. Gleich werden wir uns daran machen, diese Geste an einen View zu binden und ans Laufen zu bringen.
Eigene Gesten erstellen W 73
Die eigene Geste installieren Unser neuer DeleteGestureRecognizer wird auf genau die gleiche Weise verwendet wie die anderen Recognizer, mit denen wir gearbeitet haben: Wir binden ihn an einen View und definieren die Aktion, die aufgerufen wird, wenn der View eine Geste erkennt. Mit der Aktion werden wir uns gleich befassen, zuvor aber müssen wir überlegen, an welchen View wir die Geste binden wollen. Binden wir ihn an die einzelnen Luftblasen, muss die Geste in der Luftblase selbst beginnen, damit die Berührung diesem View zugeordnet wird. Natürlicher aber ist es, eine derartige Löschen-Geste um das Bild herum auszuführen – nicht unbedingt darin. Binden wir die Löschen-Geste also an den View des Bubbles5ViewController, der die Luftblasen enthält, damit wir die Geste an einem beliebigen Ort beginnen können. Natürlich müssen wir dann irgendwann herausfinden, welcher Image-View gelöscht werden soll – aber das sollte jetzt kein Problem mehr sein. Etwas Ähnliches haben wir bereits bei unseren Tap-Gesten für Luftblasen gemacht. Lassen Sie uns zunächst den Code betrachten, mit dem unsere Geste an den View des BubblesViewController gebunden wird. gestures/Bubbles5/Classes/Bubbles5ViewController.m
- (void)viewDidLoad { [super viewDidLoad]; // Hier steht der bisherige Code. DeleteGestureRecognizer *deleteRecognizer = [[DeleteGestureRecognizer alloc] initWithTarget:self action:@selector(handleDeleteFrom:)]; [self.view addGestureRecognizer:deleteRecognizer]; [deleteRecognizer release];
X X X X X X }
All das ist Ihnen bereits begegnet: Das einzig Bemerkenswerte ist die Verwendung einer Aktion namens handleDeleteFrom. Sorgen wir für ihre Implementierung: gestures/Bubbles5/Classes/Bubbles5ViewController.m
- (void)handleDeleteFrom:(DeleteGestureRecognizer *)recognizer { if (recognizer.state == UIGestureRecognizerStateRecognized) { UIView *viewToDelete = [recognizer viewToDelete]; [viewToDelete removeFromSuperview]; } }
74 X Kapitel 3: Gesten nutzen Dieser Code ist simpel: Wir greifen uns den zu löschenden View über den Recognizer und entfernen ihn dann einfach aus dem Superview (unserem Bubbles5ViewController). Ein Problem gibt es allerdings: Wenn Sie sich noch einmal die DeleteGestureRecognizer -Implementierung ansehen, werden Sie feststellen, dass der viewToDelete-Eigenschaft des Recognizers nie ein View zugewiesen wurde. Aber da wir jetzt wissen, wo der View angebunden wird (an einen View, der Subviews mit zu löschenden Bildern enthält), haben wir eine bessere Vorstellung davon, wie wir das implementieren müssen. Hier ist der neue Code für die touchesMoved:withEvent:-Methode des DeleteGestureRecognizer: gestures/Bubbles5/Classes/DeleteGestureRecognizer.m
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesMoved:touches withEvent:event]; if (self.state == UIGestureRecognizerStateFailed) return; CGPoint nowPoint = [[touches anyObject] locationInView:self.view]; CGPoint prevPoint = [[touches anyObject] previousLocationInView:self.view]; if (strokeMovingUp == YES) { if (nowPoint.y < prevPoint.y ) { strokeMovingUp = NO; touchChangedDirection++; } } else if (nowPoint.y > prevPoint.y ) { strokeMovingUp = YES; touchChangedDirection++; } if (viewToDelete == nil) { UIView *hit = [self.view hitTest:nowPoint withEvent:nil]; if (hit != nil && hit != self.view) { self.viewToDelete = hit; } }
X X X X X X }
Gehen wir das einmal durch. Wurde der zu löschende View noch nicht ermittelt, nehmen wir alle Punkte, die der Finger während der Bewegung besucht hat, und führen dann einen Hit-Test auf dem View des Controllers durch. Denken Sie daran, dass uns ein Hit-Test den untersten View in der View-Hierarchie liefert, der diesen Ort enthält. Gibt es keinen View oder ist es nur der View des Controllers, haben wir keinen Image-View gefunden. Andernfalls haben wir den View gefunden, den der Finger des Benutzers löscht. Das ist der gesuchte View, den wir viewToDelete zuweisen.
Was hat denn da geknallt? W 75 Dies war unser letzter Code. Unsere Luftpolsterfolienanwendung kann jetzt kompiliert und getestet werden. Erstellen Sie also ein paar Luftblasen und löschen Sie sie.
Den Löschen-Recognizer vertiefen Wir haben gerade mit erstaunlich wenig Code einen eigenen Recognizer geschrieben und in unsere Luftpolsterfolien-App integriert. Allerdings stecken in dieser kleinen Implementierung eine Menge Überlegungen und eine ganze Menge Technologie. Lassen Sie uns die Sache deswegen noch einmal kurz durchgehen: Wir haben einen Recognizer erstellt, der die Bewegung eines Fingers über die y-Achse beobachtet. Werden mehr Finger eingesetzt, schlägt die Erkennung fehl, und reset wird aufgerufen, damit die nächste Folge von Multi-Touch-Events untersucht werden kann. Wenn der Benutzer seinen Finger hebt, bevor die Bewegung mindestens drei Mal die Richtung geändert hat, wird der Zustand ebenfalls auf Fehlschlag gesetzt. Handelt der Benutzer jedoch wie erwartet, wird der Erkannt-Zustand gesetzt. Im Hintergrund übernimmt dann die Oberklasse UIGestureRecognizer und kümmert sich darum, dass die Aktionsnachricht an unser Ziel gesendet wird (denken Sie daran, dass das Ziel das Objekt ist, das darauf wartet, dass es benachrichtigt wird, wenn die Geste erkannt wird). Außerdem hält unser Code die Bewegung des Benutzers nach und speichert eine Referenz auf den ersten UIImageView, über den der Finger fährt, um diesen View dann zu löschen, wenn die Geste erfolgreich erkannt wird. Der Code ist nicht perfekt. Zunächst ist die Logik für die Geste nicht sonderlich raffiniert. Aber ist das denn unbedingt notwendig? Manchmal funktioniert eine einfache Heuristik erstaunlich gut. Was passiert jedoch, wenn einige Luftblasen nah beieinander sind? Löscht der Code alle? Nein – er löscht nur den ersten UIImageView, über den der Finger des Benutzers fährt. Wie können wir das beheben? Und abschließend: Was ist, wenn die Anwendung ein Wischen mit einem Löschen verwechselt? Jetzt ist es Zeit, dass wir uns all diesen Problemen zuwenden.
3.7
Was hat denn da geknallt? Finden Sie es ebenso unbefriedigend wie wir, dass es kein Geräusch gibt, wenn wir eine Blase platzen lassen? Bringen wir die Folie zum Knallen. Wenn Sie das Projekt Bubbles5conflict öffnen (der Name leitet sich aus dem Namen des nächsten Abschnitts ab), werden Sie feststellen, dass wir bereits eine bubble.aif-Klangdatei für Sie bereitgestellt
76 X Kapitel 3: Gesten nutzen haben. Damit unser Code diese Audiodatei abspielt, wenn auf dem Bildschirm eine Blase zum Platzen gebracht wird, werden wir die Klasse AVAudioPlayer nutzen, die gut für Audiodateien im Bundle geeignet ist (beispielsweise im Gegensatz zu Audiodaten, die über das Netzwerk gestreamt werden), die wir einfach zum Abspielen einsetzen, ohne dabei die Audiodaten zu verarbeiten oder zu mischen. Wenn Sie die Klasse AVAudioPlayer nutzen wollen, müssen Sie Ihrem Projekt das AVFoundation-Framework hinzufügen und in die Bubbles5ViewController.h-Header-Datei den folgenden Import einschließen: #import
Da Sie gerade dabei sind, können Sie gleich auch noch eine Instanzvariable für den Player selbst hinzufügen: AVAudioPlayer *player;
Und ein paar Methodendeklarationen: -(void)preparePopSound; -(void)makePopSound;
Die erste Methode werden wir nutzen, um den Audio-Player einzurichten, und die zweite werden wir jedes Mal aufrufen, wenn wir das Platzgeräusch hören wollen. Definieren wir jetzt die Methode preparePopSound: gestures/Bubbles5conict/Classes/Bubbles5ViewController.m
-(void)preparePopSound { NSURL *url = [NSURL fileURLWithPath: [NSString stringWithFormat:@"%@/bubble.aif", [[NSBundle mainBundle] resourcePath]]]; NSError *error; player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error]; player.numberOfLoops = 0; }
Hier erstellen wir zunächst ein NSURL-Objekt, das auf die bubble.aifDatei im Bundle zeigt. Dann instantiieren wir einen AVAudioPlayer, damit wir diese URL abspielen können, und setzen die Anzahl Abspielvorgänge (Loops oder Schleifen) auf null, damit das Abspielen nicht wiederholt wird. Werfen wir jetzt einen Blick auf die Methode makePopSound::
Recognizer-Konflikt W 77 gestures/Bubbles5conict/Classes/Bubbles5ViewController.m
-(void)makePopSound { [player play]; }
Leichter kann es wirklich nicht werden. Wir fordern einfach den Player auf, den Klang abzuspielen. Jetzt müssen wir nur noch den Aufruf beider Methoden einfügen. Den preparePop-Sound:-Aufruf können Sie irgendwo in die viewDidLoad:Methode in Bubbles5ViewController.m einfügen. Den makePopSound:Aufruf fügen Sie einfach in handleTapFrom: in den Code ein, in dem festgestellt wird, dass die Blase zum Platzen gebracht wurde. Das sieht so aus: gestures/Bubbles5conict/Classes/Bubbles5ViewController.m
-(void)handleTapFrom:(UITapGestureRecognizer *)recognizer { CGPoint location = [recognizer locationInView:self.view]; UIView *hitView = [self.view hitTest:location withEvent:nil]; if ([hitView isKindOfClass:[UIImageView class]]) { [(UIImageView *)hitView setImage:[UIImage imageNamed:@"popped.png"]]; [self makePopSound]; } else { //Hier folgt der Rest von handleTapFrom. }
X
}
Nachdem wir festgestellt haben, dass eine Luftblase getroffen wurde, ändern wir das Bild und rufen dann makePopSound: auf. Fügen Sie das in Ihren Code ein und probieren Sie es aus. Ist das nicht erheblich amüsanter? Wahrscheinlich ist Ihnen aufgefallen, dass man Luftblasen mehrfach platzen lassen kann – das überlassen wir Ihnen als Übung. Kehren wir jetzt zu den Gesten zurück.
3.8
Recognizer-Konflikt Führen Sie erneut die Luftpolsterfolien-App aus und versuchen Sie, eine Luftblase mit der Geste kaputt zu machen, die Sie in Abbildung 3.6 auf der nächsten Seite sehen. Wir versuchen hier, eine Löschen-Geste auszuführen, aber lassen zu Anfang einen Finger ein gutes Stück nach rechts gleiten. Zunächst sieht das nach einem Wischen aus, aber dann schieben wir die für das Löschen erforderlichen Bewegungen nach – und erwarten eigentlich, dass das auch so funktioniert. Erfahrungsgemäß
78 X Kapitel 3: Gesten nutzen werden Sie jedoch feststellen müssen, dass diese Geste häufig (nicht immer) als ein Wischen betrachtet und die gesamte Folie gelöscht wird.
Abbildung 3.6: Löschen und Wischen in Konflikt Zum Abschluss dieses Kapitels werden wir dieses Verhalten reparieren, indem wir sicherstellen, dass die Löschen-Geste eine Gelegenheit erhält, erkannt zu werden, bevor die Wisch-Geste die Erkennung abschließt. Wenn wir verstehen wollen, wie das funktioniert, müssen wir uns zunächst ansehen, wie zwei Gesten auf dem gleichen View operieren. In unserer Anwendung haben wir mehrere Gesten im Haupt-View installiert: eine Ein-Finger-Tap-Geste, eine Swipe-Geste und eine eigene Löschen-Geste. Wenn Touch-Events empfangen werden, werden sie durch alle drei Recognizer geleitet, bis einer von ihnen die entsprechende Geste erkennt. In diesem Beispiel sieht die Swipe-Geste ein Touch-Event, das ihr sagt, dass das ein Wischen ist. Sie erkennt das Wischen, noch bevor wir überhaupt zum Löschen-Teil unserer Geste übergehen. Bei manchen Implementierungen kann dieses Verhalten gewünscht sein, hier jedoch nicht. Deswegen wollen wir die Recognizer etwas ändern, damit ein einzelnes Löschen Vorrang vor dem Wischen hat.
Recognizer-Konflikt W 79 Dazu werden wir eine neue Methode der UIGestureRecognizer -Oberklasse einführen, requireGestureRecognizerToFail:, mit der wir angeben können, dass der eine Recognizer fehlschlagen muss, bevor der andere überhaupt die Möglichkeit erhält, einen Stream von MultiTouch-Events zu verarbeiten. Die Methode requireGestureRecognizerToFail: kann auf jedem Recognizer aufgerufen werden und hat einen Parameter: einen anderen Recognizer. Hier ist ein Beispiel für ihre Verwendung: [secondRecognizer requireGestureRecognizerToFail:firstRecognizer]
Die Methode bewirkt, dass secondRecognizer im UIGestureRecognizerStatePossible-Zustand bleibt, bis firstRecognizer den Zustand UIGestureRecognizerStateFailed erreicht. Tut firstRecognizer das nicht und erreicht den Zustand UIGestureRecognizerStateRecognized oder UIGestureRecognizerStateBegan, ändert secondRecognizer seinen Zustand in UIGestureRecognizerStateFailed. Erreicht firstRecognizer seinen UIGestureRecognizerStateFailed-Zustand, erhält secondRecognizer die Möglichkeit, die Multi-Touch-Events zu prüfen. Machen wir uns mit diesem neuen Wissen daran, unserem LöschenRecognizer die Möglichkeit zu geben, seine Geste zu erkennen, bevor der Swipe-Recognizer seine Untersuchung der Touch-Events beginnt. Dazu werden wir die Methode requireGestureRecognizerToFail: auf dem Swipe-Recognizer aufrufen müssen und ihr den Löschen-Recognizer übergeben. Nach all diesen Erwägungen ist die eigentliche Entwicklung recht einfach – Sie müssen in viewDidLoad: nur eine einzige Zeile unter dem Code einfügen, der die Swipe- und Löschen-Gesten erzeugt: gestures/Bubbles5conict/Classes/Bubbles5ViewController.m
-(void)viewDidLoad { [super viewDidLoad]; // Hier steckt der alte Gestencode. DeleteGestureRecognizer *deleteRecognizer = [[DeleteGestureRecognizer alloc] initWithTarget:self action:@selector(handleDeleteFrom:)]; [self.view addGestureRecognizer:deleteRecognizer]; [swipeRecognizer requireGestureRecognizerToFail:deleteRecognizer]; [deleteRecognizer release]; [swipeRecognizer release];
X X X }
80 X Kapitel 3: Gesten nutzen
Zusatzaufgabe: Eine geplatzte Blase platzen lassen? In der aktuellen Implementierung können Sie eine bereits geplatzte Blase platzen lassen. Legen Sie los und probieren Sie es aus. Sehen Sie? Ihre Zusatzaufgabe besteht darin, den Code leicht zu ändern, um sicherzustellen, dass das nicht passieren kann. Wie würden Sie das bewerkstelligen?
Wie bereits besprochen, sagen wir hier dem swipeRecognizer, dass deleteRecognizer fehlschlagen muss, bevor er damit beginnen darf, sich an der Gestenerkennung zu versuchen. Beachten Sie, dass Sie auch die Freigabe der beiden Gesten unter den Aufruf von requireGestureRecognizerToFail: verschieben sollten. Kompilieren Sie Ihren Code und probieren Sie ihn aus.
3.9
Zusammenfassung In diesem Kapitel haben wir nur wenig eigenen Code geschrieben und dennoch, obwohl das Beispiel einfach ist, einige mächtige Einsatzmöglichkeiten für Gesten vorgeführt. Sie werden feststellen, dass es ebenso einfach ist, Ihren eigenen Anwendungen ähnliche Verhalten hinzuzufügen. Obwohl wir uns in diesem Kapitel die Höhepunkte von Apples neuer Gesten-API angesehen haben, sollten Sie sich, wenn Sie das Verhalten von Views, Gesten und Multi-Touch präziser steuern wollen, die zusätzlichen Low-Level-Aufrufe ansehen, mit denen Sie diese Dinge genauer steuern können (mehr Informationen finden Sie im iOS Application Programming Guide [App10a]). Aber in den meisten Fällen werden Sie nicht derart tief eintauchen müssen. Insgesamt sehen Sie, dass die neue Gesten-API fast immer die Komplexität reduziert, die das Schreiben von Gesten impliziert. Und wahrscheinlich entdecken Sie bereits, wie entscheidend Gesten beim Entwurf der digitalen Freuden für das iPad sind.
Kapitel 4
Popover und modale Dialoge Gelegentlich müssen Benutzer mehr Informationen eingeben, als mit einer einzelnen Geste transportiert werden können. Vielleicht müssen sie eine Option in einer Auswahl wählen, um die Farbe einer Komponente, die Schrift eines Texts oder die zu ladende Webseite zu bestimmen. In diesem Kapitel werden wir eine einfache Anwendung erstellen, die einen Frachtcontainer darstellt, dessen Farbe der Nutzer selbst bestimmen kann. Das ist keine sonderlich aufregende App – gegen Ende werden wir sie mit einer Schaltfläche etwas aufmöbeln, über die der Benutzer den Frachtcontainer auf dem Bildschirm bewegen kann. Der Anwender kann den Container auswählen, indem er darauf tippt. Aber was dann? Wir könnten mit einfachen Gesten einen vordefinierten Satz von Farben durchlaufen. Aber das ist nicht eben ein Vergnügen. In einer iPhone-Anwendung würden wir normalerweise einen modalen View auf den Bildschirm schieben und zur Aufnahme der Informationen nutzen. Die gleiche Technik können wir auch auf dem iPad nutzen. Zu Anfang dieses Kapitels werden wir uns ansehen, wie man das macht und wie man einige der iPad-spezifischen Optionen für modale Views nutzt.1
1 Rufen Sie sich ins Gedächtnis, dass ein modaler View verlangt, dass Sie eine Aktion ausführen und den View schließen, bevor Sie wieder mit den anderen Elementen der Anwendung interagieren können.
82 X Kapitel 4: Popover und modale Dialoge Modale Views sind nicht immer die beste Wahl. Die gleichen Möglichkeiten könnten wir mithilfe des neuen iPad-Popover nutzen, das Sie in Kapitel 2, Split-Views, kennengelernt haben. Es wird sich herausstellen, dass beides auf recht ähnliche Weise implementiert wird. Entscheidend aber ist, dass Sie überlegen, wann ein modaler View die bessere Wahl ist und wann man besser ein Popover nutzen sollte. In diesem Kapitel werden wir uns sowohl mit der Implementierung als auch mit der Entscheidung befassen. Außerdem werden wir uns die Zeit nehmen, zu verstehen, was im Hintergrund passiert, wenn der Split-ViewController so eingerichtet ist, dass er Popover nutzt, wie wir es im Portrait-Modus sahen. Die Grundtechniken für modale Views und Popover sind ähnlich: 앫
Ein Event überwachen. Wir werden das Tippen auf unsere Vorratsdose und das Drücken eines Buttons überwachen.
앫
Einen View-Controller und eine damit verbundene Nib erstellen, die den Inhalt des View enthält, der entweder modal oder als Popover angezeigt wird.
앫
Bei Empfang des Events den modalen View oder das Popover anzeigen.
앫
Auf die Nutzerinteraktion mit dem Controller antworten und View oder Popover schließen, wenn der Benutzer die Arbeit damit abgeschlossen hat.
Legen wir los.
4.1
Auf Berührung reagieren Als Einstiegshilfe haben wir für Sie ein Beispielprojekt in die CodeDownloads gepackt. Öffnen Sie das Projekt im Ordner Cargo1, in dem Sie eine View-basierte iPad-App namens Cargo finden. Bislang haben wir nur die CargoViewController -Nib geöffnet, einen Subview mit einem roten Hintergrund erstellt, der unseren Frachtcontainer darstellt, und eine Toolbar mit einem Bewegen-Button eingefügt.
Auf Berührung reagieren
Binden wir nun eine Tap-Geste an unsere Frachtcontainer. Fügen Sie in CargoViewController.h ein Outlet namens cargoView ein und binden
Sie es an unseren Container. Nutzen Sie die in Abschnitt 3.2, Einfache Tap-Gesten nutzen, vorgestellte Technik, um die Methode cargoContainerDidGetTapped für Taps auf den Frachtcontainer zu registrieren. autorelease sorgt dafür, dass der Gesten-Recognizer unmittelbar nach der Erstellung freigegeben wird, da cargoView eine Referenz darauf hält. Popover/Cargo2/Classes/CargoViewController.m
X X X X X
- (void)viewDidLoad { [super viewDidLoad]; [cargoView addGestureRecognizer: [[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(cargoContainerDidGetTapped)] autorelease]]; }
In dieser ersten Implementierung ändern wir einfach die Farbe bei jedem Tap auf den Container: Popover/Cargo2/Classes/CargoViewController.m
-(void) cargoContainerDidGetTapped { cargoView.backgroundColor = [UIColor colorWithRed:(random()%3)/3.0 green:(random()%3)/3.0 blue:(random()%3)/3.0 alpha:1]; }
W
83
84 X Kapitel 4: Popover und modale Dialoge Ziemlich cool. Die App reagiert jetzt auf Fingerkontakt. Die Farbe des Frachtcontainers ändert sich etwas zufällig, wenn auf ihn getippt wird. Schieben wir jetzt einen modalen View ein, in dem der Benutzer aus einer kleinen Farbauswahl die Farbe auswählen kann.
4.2
Den Controller für die Farben erstellen Egal ob wir die Farbauswahl als modalen View oder als Popover einfügen, wir erzeugen einen neuen View-Controller mit einer neuen Viewbasierten Nib. Erstellen Sie eine neue Klasse namens CargoColorChooser, die eine Unterklasse von UIViewController ist. Kreuzen Sie die Kästchen an, die erforderlich sind, damit als Ziel das iPad ausgewählt und eine Nib für die Benutzerschnittstelle generiert wird. Sie können ruhig die CargoColorChooser-Nib aus dem Ordner Cargo3 nutzen oder aber selbst eine einrichten. Der View hat 200×240 Pixel, ein Label oben, sechs 40×40 Pixel große Buttons in zwei Reihen in der Mitte und einen Button unten.2
Sehen Sie diesen Schließen-Button unten im View? Das ist ein wichtiger Unterschied zwischen den Controller-Views, die Sie für einen modalen View nutzen, und denen, die Sie für ein Popover verwenden. Der Benutzer erwartet, dass sich ein Popover schließt, wenn eine Aktion abgeschlossen ist oder der Bildschirm außerhalb des Popover berührt wird. Ein modaler View muss dem Benutzer eine Möglichkeit bieten, ihn zu schließen, da keine anderen Eingaben erfolgen können, wenn ein modaler View im Fokus ist. 2 Quadratische Buttons erhalten Sie, indem Sie UIViews aus der Library ziehen, sie platzieren, färben und dann die Klasse in UIButton ändern.
Modale Views einblenden Die Klasse CargoColorChooser muss Aktionen für den Schließen-Button sowie die Farbbuttons bieten und muss mit dem UIView-Objekt für den Frachtcontainer kommunizieren. Fügen Sie dieses Outlet und die beiden Aktionen in CargoColorChooser.h ein: Popover/Cargo4/Classes/CargoColorChooser.h
#import @interface CargoColorChooser : UIViewController { IBOutlet UIView *cargoView; } -(IBAction) dismiss; -(IBAction) setCargoColor:(id)sender; @end
Das CargoColorChooser -Objekt ist eine Brücke zwischen dem HauptView der Anwendung und dem modalen Dialog; es muss seinen Fuß in beiden Welten haben. Es ist bereits der File’s Owner in der CargoColorChooser -Nib. Sie können also Ihre Buttons mit den dismiss- und setCargoColor:-Aktionen verbinden. Wählen Sie beim Verbinden das Touch Up Inside-Event, um Aktionen auszulösen. Ziehen Sie eine Instanz von CargoColorChooser in die CargoViewController -Nib und verbinden Sie das cargoView-Outlet.3 Prüfen Sie im Attributes-Inspektor den CargoColorChooser und sichern Sie, dass die Checkbox für Resize View from NIB nicht aktiviert ist. Fügen Sie in CargoViewController.h ein Outlet des Typs CargoColorChooser namens cargoColorChooser ein. Verbinden Sie dieses Outlet mit dem CargoColorChooser -Objekt in der CargoViewController-Nib. Bis zu diesem Punkt nutzen Sie im Wesentlichen die gleiche Einrichtung für modale Dialoge und Popover. Implementieren wir als Nächstes den modalen Dialog.
4.3
Modale Views einblenden Wir haben jetzt alle Teile zusammengefügt. Jetzt müssen wir nur noch ein paar letzte Handgriffe vornehmen und unseren modalen View auf den Stack schieben. Aktuell ändert die cargoContainerDidGetTappedMethode in CargoViewController.m die Farbe des Containers. Ändern Sie die Implementierung folgendermaßen, damit der modale View geöffnet wird: 3 Denken Sie daran, dass Sie CargoColorChooser, da es eine von uns selbst erstellte Klasse ist, im Classes-Tab der Library finden.
W
85
86 X Kapitel 4: Popover und modale Dialoge Popover/Cargo5/Classes/CargoViewController.m
-(void) cargoContainerDidGetTapped { [self presentModalViewController:cargoColorChooser animated:YES]; }
Erstellen Sie die Anwendung und führen Sie sie aus. Wenn Sie jetzt auf den Container tippen, sollte der modale View von unten eingeschoben werden und den Bildschirm füllen.
Es ist fast peinlich, wie einfach das ist.
Vorbereitung und Abschluss Die Gestaltung eines modalen View oder eines Popover ähnelt der Arbeit eines Anstreichers. Bevor Sie damit beginnen können, die Farbe aufzubringen, müssen Sie die Oberfläche vorbereiten und alles für den Anstrich bereit machen. Der Code für das Anzeigen und Ausblenden ist recht verständlich. Ein Großteil der Arbeit steckt in der Vorbereitung des Controllers und der Nib, die angezeigt wird.
Modale Views einblenden
W
Außerdem müssen wir den modalen View schließen, wenn der Benutzer auf den Schließen-Button klickt. Das tun wir in der dismiss-Aktion in CargoColorChooser.m. Popover/Cargo5/Classes/CargoColorChooser.m
-(IBAction) dismiss { [self dismissModalViewControllerAnimated:YES]; }
Implementieren wir zum Abschluss die Aktion zur Änderung der Farbe des Containers auf Basis dessen, was der Benutzer im modalen Dialog auswählt. Popover/Cargo5/Classes/CargoColorChooser.m
-(IBAction) setCargoColor:(id)sender{ cargoView.backgroundColor = ((UIView *) sender).backgroundColor; }
Erstellen Sie die App und führen Sie sie aus. Jetzt können Sie den modalen View mit einem Tipp auf den Container öffnen, eine Farbe wählen und den modalen Dialog schließen, um den Container in seiner neuen Farbe zu sehen. Beachten Sie, dass der modale View den gesamten Bildschirm einnimmt, obwohl wir in der CargoColorChooser -Nib einen kleinen View erstellt haben. Beim iPad können Sie andere Optionen wählen, die die Größe des angezeigten modalen View steuern. Dazu können Sie die modalPresentationStyle-Eigenschaft des einzublendenden View-Controllers setzen. Fügen Sie beispielsweise die unten in der cargoContainerDidGetTapped-Implementierung hervorgehobene Zeile ein: Popover/Cargo6/Classes/CargoViewController.m
-(void) cargoContainerDidGetTapped { cargoColorChooser.modalPresentationStyle = UIModalPresentationFormSheet; [self presentModalViewController:cargoColorChooser animated:YES]; }
Die Option UIModalPresentationFormSheet führt zu einem kleineren modalen View in der Mitte des Bildschirms bei abgeblendetem Hintergrund, damit die Benutzer wissen, dass sie mit nichts interagieren sollen, das sich nicht auf dem modalen View befindet.
87
88 X Kapitel 4: Popover und modale Dialoge
Wählt der Benutzer jetzt eine Farbe, sieht er unmittelbar das Ergebnis und muss nicht warten, bis der modale View geschlossen wird. Das ist natürlich nicht zwangsläufig so. Befände sich der Container in der Mitte des Bildschirms, würde er selbst durch diesen kleinen modalen View verdeckt. Eine Lösung ist, den Hintergrund des View des CargoColorChooser transparent zu machen. In unserem Fall ist ein Popover die bessere Lösung.
4.4
Den Controller bereinigen Benutzer sollten nie gezwungen werden, irgendeinen Button zu nutzen, um ein Popover zu schließen. Das Popover sollte verschwinden, wenn der Benutzer außerhalb des Popover auf den Bildschirm klickt oder auch eine Aktion abschließt, vorausgesetzt, das passt zu dieser Aufgabe. In unserem Beispiel könnten wir das Popover schließen, wenn der Benutzer eine Farbe auswählt, oder wir könnten es offen halten, damit er eine Farbe auswählen, einen Blick auf das Ergebnis werfen und sich eventuell doch für eine andere Farbe entscheiden kann. Sie sollten jeweils das Verhalten wählen, das den Erwartungen der Benutzer am ehesten entspricht. In einer Sache sind Apples iOS Human Interface Guidelines [App10c] eindeutig: Popover sollten nie Schließen-Buttons enthalten. Entfernen Sie den Schließen-Button aus dem View des CargoColorChooser. Und da Sie gerade dabei sind, entfernen Sie gleich auch die dismiss-Aktion aus der Klasse CargoColorChooser.
Ein Popover anzeigen W 89 Die meisten anderen Entscheidungen sind Geschmackssache. Wir werden das Popover neben dem Frachtcontainer erscheinen lassen und ihm einen Pfeil geben, der auf den Container zeigt. Wahrscheinlich könnten wir auch die Überschrift „Container-Farbwähler“ entfernen und nur sechs Farben anzeigen lassen. Eine weitere Möglichkeit besteht darin, die Größe der Buttons zu ändern und sie vertikal in einer einzigen Spalte anzuordnen. Wir werden die Überschrift entfernen und den View mit den Buttons verkleinern, damit er sich besser an sie anschmiegt.
Bevor wir nun das Popover erstellen, sollten Sie diesen Code ausführen, um sich den Unterschied zu vergegenwärtigen zwischen dem, was wir in einem modalen View anzeigen müssen und was in einem Popover. In einem modalen View funktioniert es einfach nicht, bloß sechs farbige Quadrate anzuzeigen. Außerdem gibt es keine Möglichkeit, den modalen View zu verlassen, wenn wir uns einmal in ihm befinden. Im Gegensatz dazu funktioniert genau das bei einem Popover ausgezeichnet, wie wir gleich sehen werden.
4.5
Ein Popover anzeigen Ein Popover anzuzeigen, ist etwas komplizierter, als einen modalen View anzuzeigen. Wir können den Controller auf unterschiedliche Weise anpassen, um zu beschreiben, wo er sich im Verhältnis zu dem Objekt befindet, das der Benutzer antippte, um es zu öffnen. Zunächst erstellen wir einen UIPopoverController. Zur Konfigurierung von init müssen wir sagen, welcher View-Controller für den Inhalt verwendet wird. Ein Popover-Controller selbst ist kein View-Controller, er beherbergt einen View-Controller. Hier haben wir die Eigenschaft popoverContentSize gesetzt, damit das Popover die gleiche Größe wie der View hat, den wir in der CargoColorChooser-Nib konfiguriert haben. Wenn das Popover angezeigt werden soll, brauchen wir eine Möglichkeit, das visuelle Element anzugeben, auf das der Benutzer getippt hat,
90 X Kapitel 4: Popover und modale Dialoge damit der Pfeil vom Popover auf dieses Element zeigt. Vielleicht zeigen Sie das Popover gar an, um genau den Teil des View anzupassen, auf den Sie zeigen. Sie können einen bestimmten Bereich in diesem View anzeigen, indem Sie das anvisierte Rechteck setzen. Hier modifizieren wir den gesamten Frachtcontainer, setzen das Rechteck also auf cargoView.bounds. Popover/Cargo8/Classes/CargoViewController.m
-(void) cargoContainerDidGetTapped { UIPopoverController *popover = [[UIPopoverController alloc] initWithContentViewController:cargoColorChooser]; popover.popoverContentSize = cargoColorChooser.view.frame.size; [popover presentPopoverFromRect:cargoView.bounds inView:cargoView permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES]; }
Erstellen Sie die App und führen Sie sie aus. Wenn Sie auf den Frachtcontainer tippen, müsste etwas Ähnliches zu sehen sein, was die folgende Abbildung zeigt:
Die Richtung des Pfeils haben wir nicht angegeben. Für permittedArrowDirections haben wir den Wert UIPopoverArrowDirectionAny übergeben. Nutzen Sie diese Option, wenn das System selbst herausfinden kann, wo das Popover angezeigt werden sollte. Auch wenn es gute Gründe dafür gibt, eine bestimmte Richtung anzugeben, sollten Sie mehrere Optionen anbieten. Hier zeigen wir beispielsweise an, dass es okay ist, das Popover links oder rechts des View einzublenden:
Ein Popover anzeigen W 91 Popover/Cargo8a/Classes/CargoViewController.m
-(void) cargoContainerDidGetTapped { UIPopoverController *popover = [[UIPopoverController alloc] initWithContentViewController:cargoColorChooser]; popover.popoverContentSize = cargoColorChooser.view.frame.size; [popover presentPopoverFromRect:cargoView.bounds inView:cargoView X permittedArrowDirections:UIPopoverArrowDirectionLeft + X UIPopoverArrowDirectionRight animated:YES]; }
Da der Container hier dem linken Rand zu nahe ist, wird das Popover auf der rechten Seite angezeigt. Auch wenn das Geschmackssache ist, scheint dieses Popover neben dem View besser aufgehoben als über oder unter ihm.
Nachdem wir das Popover zum Erscheinen gebracht haben, müssen wir es auch wieder verschwinden lassen. Aktuell kann der Benutzer das Popover zum Verschwinden bringen, indem er irgendwo auf den Bildschirm klickt. Es kann also kein großer Aufwand sein, das Popover verschwinden zu lassen, wenn eine Farbe ausgewählt wird. Sie werden ein Handle auf den Popover-Controller benötigen. Fügen Sie der Klasse CargoColorChooser eine Eigenschaft namens popoverController mit dem Typ UIPopoverController hinzu. Setzen Sie diese Eigenschaft, indem Sie der cargoContainerDidGetTapped-Methode in CargoViewController.m die unten hervorgehobene Zeile hinzufügen:
92 X Kapitel 4: Popover und modale Dialoge Popover/Cargo9/Classes/CargoViewController.m
-(void) cargoContainerDidGetTapped { UIPopoverController *popover = [[UIPopoverController alloc] initWithContentViewController:cargoColorChooser]; popover.popoverContentSize = cargoColorChooser.view.frame.size; X cargoColorChooser.popoverController = popover; [popover presentPopoverFromRect:cargoView.bounds inView:cargoView permittedArrowDirections:UIPopoverArrowDirectionLeft + UIPopoverArrowDirectionRight animated:YES]; }
Verwerfen Sie den Popover-Controller, nachdem eine Farbe ausgewählt wurde: Popover/Cargo9/Classes/CargoColorChooser.m
-(IBAction) setCargoColor:(id)sender{ cargoView.backgroundColor = ((UIView *) sender).backgroundColor; X [self.popoverController dismissPopoverAnimated:YES]; }
4.6
Ein erneuter Blick auf Split-View und Popover Erinnern Sie sich an den Button, den wir in Abschnitt 2.8, Ein Popover einfügen, unserem Detail-View hinzugefügt haben, wenn der Split-View vom Landscape- zum Portrait-Modus wechselte? Jetzt wissen Sie genug, um besser zu verstehen, was dort ablief. In unserem Beispiel wurde der RootViewController verborgen, als das iPad gedreht wurde. Deswegen haben wir seinen Inhalt in ein Popover gepackt, das erschien, wenn auf den Button in der Werkzeugleiste geklickt wurde. Wenn sich das iPad in den Portrait-Modus dreht, wird die folgende Delegate-Methode aufgerufen: SplitVC/DailyShootiPad/Classes/DetailViewController.m
-(void)SplitViewController: (UISplitViewController*)svc willHideViewController:(UIViewController *)aViewController withBarButtonItem:(UIBarButtonItem*)barButtonItem forPopoverController: (UIPopoverController*)pc { barButtonItem.title = aViewController.title; NSMutableArray *items = [[toolbar items] mutableCopy]; [items insertObject:barButtonItem atIndex:0]; [toolbar setItems:items animated:YES]; [items release]; self.popoverController = pc; }
Popover für Buttons Diese Methode gibt uns ein Handle auf alles, was wir benötigen, aber die meiste Arbeit erfolgt an einem anderen Ort. Der Body der Methode besteht im Wesentlichen aus der Konfiguration des Werkzeugleisten-Buttons und dem Hinzufügen zur Werkzeugleiste. Bevor diese Methode aufgerufen wird, wird als Inhalt des Popover der View-Controller bestimmt, der verborgen werden soll. Die Folge war, dass der Inhalt des RootViewController von Zauberhand erschien, wenn auf den Button gedrückt wurde.
Wir müssen eigentlich nicht viel tun, um selbst ein Popover an einen Button zu binden. Deswegen wollen wir das auch als Nächstes tun.
4.7
Popover für Buttons Wir sahen in unserem Beispiel, dass man die Farbe des Containers gleichermaßen gut aus einem modalen View wie auch aus einem Popover setzen kann. Aber wenn man nicht notwendigerweise einen modalen Dialog benötigt, sollte man auch keinen verwenden. Anders gesagt: Wir ziehen für dieses Beispiel eindeutig das Popover vor. Manchmal ist die Entscheidung noch eindeutiger. Stellen Sie sich beispielsweise vor, wir hätten einen Popover-Controller, der genutzt werden kann, um den Container auf dem Bildschirm herumzuschieben.
W
93
94 X Kapitel 4: Popover und modale Dialoge Die nächste Phase unseres Projekts befindet sich im Ordner Cargo10. Sie finden darin einen zusätzlichen View-Controller namens CarDriver und die dazugehörige Nib-Datei. Der View in der Nib sieht folgendermaßen aus:
Binden wir diesen an den Bewegen-Button. Fügen Sie der Klasse CargoViewController ein Outlet für CarDriver namens carDriver hinzu. Ziehen Sie eine CarDriver -Instanz in die CargoViewController -Nib und verbinden Sie diese mit dem Outlet. Stellen Sie sicher, dass die Checkbox für „CarDriver’s Resize View from NIB“ nicht aktiviert ist. Wir benötigen eine Aktion, die aufgerufen wird, wenn auf den Button getippt wird. Deklarieren Sie eine IBAction namens showDriveControls: in CargoViewController.h. Da das das Letzte ist, das wir der Header-Datei CargoViewController hinzufügen werden, ist dies vielleicht ein guter Zeitpunkt, sie sich einmal vollständig anzusehen – viel braucht man nicht, um den Frachtcontainer und zwei Popover zu unterstützen. Popover/Cargo11/Classes/CargoViewController.h
#import @class CargoColorChooser; @class CarDriver; @interface CargoViewController : UIViewController { IBOutlet UIView *cargoView; IBOutlet CargoColorChooser *cargoColorChooser; IBOutlet CarDriver *carDriver; } -(IBAction) showDriveControls:(id)sender; @end
Verbinden Sie den Button mit der Aktion und implementieren Sie die Aktion dann folgendermaßen:
Popover für Buttons Popover/Cargo11/Classes/CargoViewController.m
-(IBAction)showDriveControls:(id)sender { UIPopoverController *controls = [[UIPopoverController alloc] initWithContentViewController:carDriver]; controls.popoverContentSize = carDriver.view.frame.size; [controls presentPopoverFromBarButtonItem:sender permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES]; }
Wie zuvor instantiieren wir den Popover-Controller und weisen ihm den Inhalt eines bestimmten View-Controllers zu, hier carDriver. Dann setzen wir die Größe des Popover. Sie sollten sich eventuell ansehen, was passiert, wenn Sie diese Zeile weglassen. Schließlich zeigen wir das Popover an und legen den Werkzeugleisten-Button fest, der mit ihm verknüpft ist, damit der Pfeil auf den Button zeigen kann, auf den der Benutzer drückte, um das Popover aufzurufen.
Schreiben Sie zur Übung Code, der auf ein Tippen auf die vier Pfeile reagiert und den Frachtcontainer in die Richtung des entsprechenden Buttons bewegt. Eine mögliche Lösung finden Sie im Download-Code in Cargo12. Was geschieht, wenn Sie auf den Button drücken, wenn das Popover bereits sichtbar ist? Es wird erneut die Methode showDriveControls aufgerufen, die den Code zur Erstellung und Anzeige des Popover-Controllers enthält. Das Problem, das das birgt, können Sie sehen, wenn
W
95
96 X Kapitel 4: Popover und modale Dialoge Sie auf den Button tippen. Tippen Sie erneut. Jetzt klicken Sie auf etwas anderes, um das Popover zu schließen. Dann sehen Sie oben links das folgende leere Popover.
Das ist nicht in Ordnung. Das Popover sollte eigentlich geschlossen werden, wenn Sie ein zweites Mal auf den Button tippen. Das erreichen Sie, indem Sie CargoViewController.h eine Variable namens driveControls mit dem Typ UIPopoverController hinzufügen. Ändern Sie den Namen der Methode showDriveControls: in showOrHideDriveControls: und reparieren Sie die Verbindung in der Nib. Implementieren Sie showOrHideDriveControls:, um das Popover einoder auszublenden. Wenn der Controller existiert, wird das Popover geschlossen und der Controller genullt. Existiert der Controller nicht, wird er erstellt, konfiguriert und angezeigt. Wir müssen uns auch um den Fall kümmern, dass der Benutzer die Richtungssteuerung schließt, indem er an anderer Stelle auf den Bildschirm klickt. Dazu setzen wir das CargoViewController -Objekt als Delegate des Popover-Controllers. Sie müssen CargoViewController.h die Deklaration des UIPopoverControllerDelegate-Protokolls hinzufügen. Wird die Delegate-Methode popoverControllerDidDismissPopover: aufgerufen, löschen wir den Popover-Controller.
Die Ausrichtung ändern Popover/Cargo13/Classes/CargoViewController.m
-(IBAction)showOrHideDriveControls:(id)sender { if (driveControls) { [driveControls dismissPopoverAnimated:YES]; driveControls = nil; } else { driveControls = [[UIPopoverController alloc] initWithContentViewController:carDriver]; driveControls.popoverContentSize = carDriver.view.frame.size; [driveControls presentPopoverFromBarButtonItem:sender permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES]; driveControls.delegate = self; } } - (void)popoverControllerDidDismissPopover: (UIPopoverController *)popoverController { driveControls = nil; }
4.8
Die Ausrichtung ändern Als wir in Kapitel 2, Split-Views, einen ersten Blick auf Popover warfen, haben wir Delegate-Methoden implementiert, um zu reagieren, wenn der Benutzer das Gerät dreht. In unserem aktuellen Beispiel haben wir keinen Split-View, wollen die Komponenten unserer Oberfläche eventuell aber trotzdem an anderer Stelle oder in anderer Größe anzeigen, je nachdem, wie der Benutzer das Gerät hält. In unserem Beispiel gibt es eigentlich keine visuellen Elemente, die umgeordnet werden müssen, deswegen fügen wir einfach vier farbige quadratische UIViews ein, damit Sie sehen können, wie diese Technik funktioniert. Setzen Sie einen UIView der Größe 100×100 Pixel in jede Ecke und stellen Sie als Hintergrundfarbe der einzelnen Quadrate jeweils eine andere Farbe ein. Nutzen Sie den Size-Inspektor, um den cargoView so einzurichten, dass er seinen Abstand zu den einzelnen Seiten bewahrt, wenn das Gerät gedreht wird. Erzeugen Sie im CargoViewController Outlets mit den Namen corner1, corner2, corner3 und corner4. Wird die App ausgeführt, tauschen wir die Quadrate der diagonal einander gegenüberliegenden Ecken aus, wenn das Gerät gedreht wird. Die Ausgangsposition kann ungefähr so aussehen:
W
97
98 X Kapitel 4: Popover und modale Dialoge
Der Schlüssel zur Koordination der Animation ist, dass wir alle zusätzlichen Animationen der willAnimateRotationToInterfaceOrientation: duration:-Methode des View-Controllers hinzufügen.4 Popover/Cargo14/Classes/CargoViewController.m
-(void)willAnimateRotationToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation duration:(NSTimeInterval)duration { if (interfaceOrientation == UIInterfaceOrientationPortrait || interfaceOrientation == UIInterfaceOrientationPortraitUpsideDown) { corner1.frame = CGRectMake(0, 44, 100, 100); corner2.frame = CGRectMake(668, 44, 100, 100); corner3.frame = CGRectMake(0, 904, 100, 100); corner4.frame = CGRectMake(668, 904, 100, 100); } else { corner4.frame = CGRectMake(0, 44, 100, 100); corner3.frame = CGRectMake(924, 44, 100, 100); corner2.frame = CGRectMake(0, 648, 100, 100); corner1.frame = CGRectMake(924, 648, 100, 100); } } 4 In diesem einfachen Beispiel haben wir die Größe und die Position der Views einfach fest vorgeschrieben. In einer Anwendung, die Sie längerfristig warten müssen, würden Sie Systemaufrufe bzw. Konstanten nutzen. Ein Beispiel dafür wird Ihnen in Abschnitt 5.5, Tastaturbenachrichtigungen nutzen, auf Seite 109 begegnen.
Die Ausrichtung ändern Die Endposition sieht nun etwa so aus:
Wir haben einfach die Start- und die Endposition der einzelnen Eckquadrate angegeben. Schauen Sie sich die Sache an, indem Sie das iPad im Simulator oder das Gerät selbst drehen und damit die Animation anstoßen. Das Popover verblasst, und die vier Eckquadrate tauschen die Position, während sich gleichzeitig die Größe des cargoView ändert. Diese Animation sieht besser aus, wenn wir den cargoView oben halten. Fügen Sie dazu der viewDidLoad-Methode in CargoViewController.m folgende Zeile hinzu: Popover/Cargo14/Classes/CargoViewController.m
[self.view bringSubviewToFront:cargoView];
Während Ihrer gesamten iPad-Entwicklung ist es wichtig, dass Sie stets im Auge behalten, wie die App in den einzelnen Ausrichtungen am besten zur Geltung kommt. In diesem Abschnitt sahen Sie ein kleines Beispiel, das Ihnen vorführte, wie man Elemente sanft verschiebt und in der Größe anpasst, wenn das Gerät gedreht wird. Derartige Animationen sind keine Spielerei (gut, in diesem Beispiel schon). Mit ihnen helfen Sie dem Benutzer, dem Element zu folgen, das ihn im Augenblick interessiert. Sie helfen ihm beim Übergang von der einen Ausrichtung zur anderen.
W
99
100 X Kapitel 4: Popover und modale Dialoge
4.9
Zusammenfassung In diesem Kapitel haben wir uns verschiedene Variationen zu modalen Views und Popovers angesehen. Wir haben einen modalen View erstellt, der den vollständigen Bildschirm bedeckt, und einen, der nur seine Mitte einnimmt und dabei die restlichen, nicht aktiven Bereiche des Bildschirms abblendet. Wir haben Popover erstellt, die erscheinen, wenn ein bestimmter Teil eines Views angetippt und wenn ein Button gedrückt wird. Wir haben uns angesehen, wie man Popover in Bezug auf die UI-Komponente positioniert, die die Anzeige des Popovers anstößt. Wir haben gesehen, wie man Popover nach einer Benutzeraktion schließt, und wir haben aus dem Popover oder dem modalen View mit einem anderen Objekt kommuniziert. Das Grundmuster dazu werden wir in unserer gesamten Entwicklung für das iPad nutzen: Wir haben eine Nib erstellt und die meiste Arbeit in dem View-Controller erledigt, der der File’s Owner für diese Nib ist. Wenn ein modaler View oder ein Popover angezeigt werden soll, müssen wir ein paar Verbindungen in beide Richtungen erstellen und den View öffnen, der den Inhalt enthält, der von unserem angepassten View-Controller verwaltet wird. Die gleiche Technik wird unter anderem auch eingesetzt, um angepasste Tabellenzellen zu erstellen.
Kapitel 5
Angepasste Tastaturen Stellen Sie sich vor, es gäbe eine für das iPhone gedachte Version von Apples Keynote- oder Pages-Apps. Was würden die leisten? Man kann sich noch vorstellen, damit vorhandene Pages-Dokumente oder Keynote-Präsentationen zu betrachten, aber es wäre ausgesprochen umständlich, wollte man auf dem winzigen Bildschirm neue Dokumente erstellen. Zwar konnte man auf dem iPhone immer Text eingeben – wie sonst soll man in Safari eine URL oder in Maps eine Adresse eingeben oder eine E-Mail beantworten? Aber obwohl es zur Not geht, wird das iPhone nicht das Gerät sein, mit dem Sie unterwegs komplizierte Dokumente bearbeiten wollen. Eins der wesentlichen Probleme ist, dass Ihr Finger auf dem iPhoneBildschirm eine Menge Platz einnimmt. Schon Craig Hockenberry schreibt: „Der iPhone-Bildschirm quetscht 160 Pixel in jeden Zoll Anzeigefläche – und Sie nutzen Ihren Finger, um mit dieser Anzeige zu interagieren. Wenn Sie Ihren Finger auf ein Lineal mit Zoll-Skala legen, werden Sie sehen, dass er am Berührungspunkt zwischen ¼ und ½ Zoll einnimmt. Das entspricht 40 bis 80 Pixel der Anzeigefläche.“1
1 Siehe http://www.alistapart.com/articles/putyourcontentinmypocket/. Craig machte diese Bemerkung in Bezug auf das iPhone 3. Mit Einführung des iPhone 4 verdoppeln sich die Zahlen. Das ändert aber nichts an dem fundamentalen Problem, dass Sie mit dem Finger einen großen Teil der Anzeige verdecken, wenn Sie den Bildschirm berühren.
102 X Kapitel 5: Angepasste Tastaturen Das bedeutet, dass der iPhone-Bildschirm ungefähr fünf Finger breit ist. Unter dieser Voraussetzung ist es schon fast erstaunlich, wie gut sich auf diesem kleinen Gerät Text eingeben lässt. Nun haben Sie gesehen, um wie viel das iPad größer als das iPhone ist. Die Tatsache zahlt sich bei der Arbeit mit der virtuellen Tastatur extrem aus. In diesem Kapitel werden Sie sehen, wie leicht man angepasste Tastaturen erstellen oder die vorhandene Tastatur bereichern kann, indem man die Text-View-Eigenschaften inputView und inputAccessoryView nutzt. Das iPad ist so groß, dass man darauf mehr tun kann, als Dokumente nur zu betrachten. Im Portrait-Modus ist der Bildschirm über ein Dutzend Finger breit, und im Landscape-Modus könnten Sie sogar noch mehr Finger unterbringen – wenn Sie denn mehr hätten. Auf dem iPad werden Sie neue Dokumente erstellen und bestehende bearbeiten wollen, wie Sie es beispielsweise mit den iPad-Versionen von Keynote, Pages und Numbers können. Es würde Ihnen nicht reichen, einfach nur Dokumente zu betrachten, die woanders erstellt wurden. Sie wissen bereits, wie Sie die neuen Techniken für Gesten und Popover einsetzen können. In diesem Kapitel werden Sie lernen, wie man angepasste Texteingaben anbietet.
5.1
Einfache Texteingabe Erstellen Sie ein View-basiertes iPad-Projekt namens Feelings. Fügen Sie der FeelingsViewController -Nib einen Text-View hinzu. Wir werden diesen Text-View nutzen, um Leuten, die sich in Ihrer unmittelbaren Nähe befinden, mithilfe von Emoticons zu zeigen, wie Sie sich fühlen. Dabei wollen wir uns auf ;-), :-), :-( und >:( beschränken.2 Der stärkeren Wirkung wegen sollten Sie die Hintergrundfarbe auf Gelb und die Schrift auf etwas Großes und Fettes wie Helvetica, fett, 288 setzen. Außerdem sollten Sie sicherstellen, dass der Text-View auch für das Wütend-Emoticon groß genug ist. Mehr brauchen Sie wirklich nicht. Erstellen Sie die App und führen Sie sie aus. Sie sollten die Emoticons jetzt eingeben können.
2
Diese Emoticons stehen für Zwinkern sowie fröhlich, traurig und wütend.
Einfache Texteingabe W 103
Noch bietet die App dem Benutzer aber keine angenehme Interaktion. Denken Sie beispielsweise an das Wütend-Emoticon. Sie müssen die Tastatur ändern, damit Sie die drei Symbole eingeben können. Aus Ihrer Zeit als iPhone-Entwickler wissen Sie, dass man die Tastatur wählen sollte, die den Bedürfnissen der Benutzers am angemessensten ist. Aber wenn Sie einen Blick auf die verfügbaren Tastaturen werfen, werden Sie feststellen, dass keine davon das leistet, was Sie benötigen.
Es gibt Tastaturen zur Eingabe von Wörtern, Zahlen, URLs und anderem, aber keine, die für unsere Emoticons geeignet ist. Machen wir uns also daran, eine eigene zu erstellen.
104 X Kapitel 5: Angepasste Tastaturen Sie werden einige der Techniken brauchen können, die Sie auch bei der Entwicklung fürs iPhone genutzt haben. Es ist immer noch von entscheidender Bedeutung, dass Sie die Tastatur auf die Daten zuschneiden, die eingegeben werden. Jetzt haben Sie auch die Möglichkeit, die Tastatur durch eine eigene, angepasste Tastatur zu ersetzen oder eine angepasste Tastatur an eine der bestehenden anzubinden. In diesem Kapitel werden Sie sehen, wie leicht sich das implementieren lässt.
5.2
Angepasste Tastaturen erstellen Wir werden eine angepasste Tastatur zur Auswahl von Emoticons erstellen. Unsere Benutzer sollen nur eine Taste drücken müssen, um ihre Gefühle auszudrücken. Zunächst benötigen wir eine neue NibDatei, die den View enthält, der dort ins Fenster gleitet, wo wir die Tastatur erwarten. Fügen Sie der Resources-Gruppe unter Verwendung der Vorlage User Interface → View XIB → iPad eine neue Datei hinzu. Geben Sie ihr den Namen MoodKeyboard. Wählen Sie den View und nutzen Sie den Attributes-Inspektor, um die Statusleiste auf Unspecied zu setzen. Im Size-Inspektor machen Sie den View 768 Pixel breit und 95 Pixel hoch. Setzen Sie die Anker und Federn so, dass der View links, rechts und unten verankert bleibt, d.h. horizontal wächst. Bestimmen Sie für die Hintergrundfarbe des View einen hellen Grauton, so wie ihn auch die bestehende Tastatur hat. Ändern Sie den Typ des File’s Owner über den Identity-Inspektor in FeelingsViewController. Sie benötigen eine PNG-Datei für jede der Emoticon-Tasten. Sie können einfach die nutzen, die Sie in den Code-Downloads im Verzeichnis Feelings2 finden. Wir haben die Tasten so entworfen, dass sie 75×75 Pixel groß sind, den gleichen Abstand haben und im View zentriert sind.
Es ist fast schon peinlich, Ihnen zu zeigen, wie man die Standardtastatur durch die von uns gerade erstellte ersetzt. Fügen Sie FeelingsViewController.h ein Outlet für den View hinzu, den Sie gerade der Nib-Datei hinzugefügt haben, und ein weiteres Outlet für den TextView.
Auf die Tasten reagieren W 105 TextInput/Feelings2/Classes/FeelingsViewController.h
#import @interface FeelingsViewController : UIViewController { } @property(nonatomic, retain) IBOutlet UIView *moodKeyboard; @property(nonatomic, retain) IBOutlet UITextView *textView; @end
Der FeelingsViewController hat drei Outlets, die den Eigenschaften entsprechen, die uns interessieren: moodKeyboard, textView und view. view sollte bereits mit dem View in FeelingsViewController.xib verbunden sein. Verbinden Sie in der gleichen Nib das textView-Outlet mit dem Text-View. Verbinden Sie das moodKeyboard-Outlet mit dem View in MoodKeyboard.xib. Jetzt haben Sie alle Vorbereitungen erledigt. Das ist der gesamte Code, der erforderlich ist, um die angepasste Tastatur einzurichten. Fügen Sie FeelingsViewController.m diese Implementierung der Methode viewDidLoad hinzu. TextInput/Feelings2/Classes/FeelingsViewController.m
- (void)viewDidLoad { [super viewDidLoad]; [[NSBundle mainBundle] loadNibNamed:@"MoodKeyboard" owner:self options:nil]; X self.textView.inputView = self.moodKeyboard; }
Nachdem Sie die Nib geladen und File’s Owner gesetzt haben, setzt die markierte Zeile die inputView-Eigenschaft des Text-View auf den View, der unsere angepassten Tasten enthält. Machen wir diese Tasten jetzt funktionsfähig.
5.3
Auf die Tasten reagieren Wir werden die Tasten so einrichten, dass ein Tippen auf eine der Emoticon-Tasten genügt, um die verschiedenen Zeichen, aus denen das Emoticon zusammengesetzt ist, auf einen Schlag einzugeben. Beispielsweise sollten Sie Folgendes sehen, wenn Sie auf die Wütend-Taste drücken:
106 X Kapitel 5: Angepasste Tastaturen
Dazu können wir eins der grundlegendsten Idiome in der Cocoa-Programmierung nutzen. Das FeelingsViewController -Objekt lebt in zwei unterschiedlichen Nib-Dateien. Wir können ihm Aktionen hinzufügen, unsere Tasten mit diesen Aktionen in der MoodKeyboard-Nib verbinden und diese dann über das Outlet in der FeelingsViewController -Nib mit dem Text-View kommunizieren lassen. Fügen Sie der FeelingsViewController-Header-Datei die folgenden Aktionen hinzu: TextInput/Feelings3/Classes/FeelingsViewController.h
-(IBAction) -(IBAction) -(IBAction) -(IBAction)
didTapWinkKey; didTapHappyKey; didTapSadKey; didTapAngryKey;
Verbinden Sie diese Aktionen mit den Emoticon-Tasten und implementieren Sie sie so, dass sie den Text im Text-View auf die entsprechende Zeichenfolge setzen und dann den First Responder abtreten.
Einen Accessory-View hinzufügen W 107 Tippt der Benutzer beispielsweise auf wütende Emoticon wird die Methode didTapAngryKey aufgerufen, die ihrerseits die Methode updateTextViewWithMood: aufruft und dabei die Zeichenfolge übergibt, die dem wütenden Emoticon entspricht. TextInput/Feelings3/Classes/FeelingsViewController.m
-(void) updateTextViewWithMood:(NSString *) mood { self.textView.text = mood; [self.textView resignFirstResponder]; } -(IBAction) didTapAngryKey{ [self updateTextViewWithMood: @">:(" ]; }
Erstellen Sie die App und führen Sie sie aus. Wenn Sie auf den TextView klicken, schiebt sich unsere angepasste Tastatur an ihren Platz. Tippen Sie auf eine der Tasten, erscheint der entsprechende Text im Text-View. Mit unserer angepassten Tastatur müssen Sie nur noch eine Taste drücken, nicht mehr drei.
5.4
Einen Accessory-View hinzufügen Man fügt eher selten nur ein Emoticon ein. In der Regel nutzt man sie, um Text hervorzuheben. Deswegen würden wir unsere angepasste Tastatur eigentlich gern neben der Standardtastatur anzeigen lassen. Es stellt sich heraus, dass wir dazu nur eine einzige Zeile Code ändern müssen: TextInput/Feelings4/Classes/FeelingsViewController.m
- (void)viewDidLoad { [super viewDidLoad]; [[NSBundle mainBundle] loadNibNamed:@"MoodKeyboard" owner:self options:nil]; X self.textView.inputAccessoryView = self.moodKeyboard; }
Wenn Sie jetzt den Text-View aktivieren, öffnet sich die Standardtastatur zusammen mit unserer angepassten Tastatur. Betrachten Sie die angepasste Tastatur als eine Ergänzung der Standardtastatur. Wie Sie im Code sehen, müssen Sie lediglich die inputAccessoryView-Eigenschaft des Text-View statt der inputView-Eigenschaft setzen. Wir wollen ein paar Anpassungen an unserem Text-View vornehmen, um einige zusätzliche Arbeiten hervorzuheben, die erledigt werden müssen. Setzen Sie die Hintergrundfarbe des View auf Rot. Ändern Sie
108 X Kapitel 5: Angepasste Tastaturen die Schriftgröße für den Text-View vom aktuellen Wert 288 auf 72. Setzen Sie die Hintergrundfarbe auf Weiß. Und, das Wichtigste, passen Sie die Größe des Text-View so an, dass er fast den vollständigen sichtbaren Bereich einnimmt. Lassen Sie auf allen Seiten einen Rand von 20 Pixeln, damit Sie den Text-View eingerahmt von der roten Hintergrundfarbe des View sehen. Passen Sie zum Abschluss die Methode updateTextViewWithMood: so an, dass der Emoticon an den bestehenden Text anhängt wird, statt ihn zu ersetzen, und dass First Responder nicht mehr abgetreten wird, nachdem ein Emoticon eingegeben wurde. TextInput/Feelings4/Classes/FeelingsViewController.m
-(void) updateTextViewWithMood:(NSString *) mood { self.textView.text = [NSString stringWithFormat:@"%@ %@ ", self.textView.text, mood]; }
Erstellen Sie die App und führen Sie sie aus. Zunächst scheint das alles ziemlich gut zu sein. Wie Sie hier sehen können, gibt es jedoch ein kleines Problem:
Tastaturbenachrichtigungen nutzen W 109 Der untere Teil des Text-View wird durch die Tastatur verborgen. Wenn Sie weitertippen, sind die Zeichen, die Sie eingeben, irgendwann nicht mehr sichtbar. Wir müssen die Größe des Text-View ändern, wenn die Tastatur erscheint oder verschwindet.
5.5
Tastaturbenachrichtigungen nutzen Denken Sie daran, dass im Hintergrund permanent Informationen herumhuschen. Benachrichtigungen werden gesendet, wenn die Tastatur erscheinen oder verschwinden soll. Wir müssen uns nur für den Empfang dieser Benachrichtigungen registrieren.3 In viewDidLoad registrieren wir uns für UIKeyboardWillShowNotication und UIKeyboardWillHideNotication und geben die Methoden an, die aufgerufen werden, wenn diese Notifikationen gesendet werden. TextInput/Feelings5/Classes/FeelingsViewController.m
- (void)viewDidLoad { [super viewDidLoad]; X [[NSNotificationCenter defaultCenter] X addObserver:self X selector:@selector(keyboardWillAppear:) X name:UIKeyboardWillShowNotification X object:nil]; X [[NSNotificationCenter defaultCenter] X addObserver:self X selector:@selector(keyboardWillDisappear:) X name:UIKeyboardWillHideNotification X object:nil]; [[NSBundle mainBundle] loadNibNamed:@"MoodKeyboard" owner:self options:nil]; self.textView.inputAccessoryView = self.moodKeyboard; }
Hier haben wir uns beim Standard-Notifikationszentrum für eine Benachrichtigung registriert, wenn UIKeyboardWillShowNotication gemeldet wird. Wir haben diese Instanz des FeelingsViewController (also self) als Beobachter und angegeben, dass die Notifikation an keyboardWillAppear: gesendet wird. Anders ausgedrückt: Wenn es eine UIKeyboardWillShowNotication gibt, sendet uns das Standard-Notifikationszentrum eine Nachricht folgender Form: [myFeelingsViewController keyboardWillAppear:notification];
3 Sollten Sie sich nicht mehr erinnern können, wie man mit Notifikationen arbeitet, finden Sie in Cocoa-Programmierung: Der schnelle Einstieg für Entwickler [Ste09] eine Einführung und Beispiele.
110 X Kapitel 5: Angepasste Tastaturen Wir müssen die keyboardWillAppear:- und die korrespondierende keyboardWillDisappear:-Methode implementieren. Implementieren wir beide Methoden zunächst so, dass die Notifikationen protokolliert werden, damit wir uns ansehen können, was uns zur Verfügung steht: TextInput/Feelings5/Classes/FeelingsViewController.m
-(void)keyboardWillAppear:(NSNotification *)notification { NSLog(@"Tastatur erscheint:\n %@", notification); } -(void)keyboardWillDisappear:(NSNotification *) notification { NSLog(@"Tastatur verschwindet:\n %@", notification); }
Erstellen Sie die App und führen Sie sie aus. Wenn Sie auf den TextView tippen, sehen Sie im Log etwa Folgendes:4 Tastatur erscheint: NSConcreteNotification 0x51195a0 {name = UIKeyboardWillShowNotification; userInfo = { UIKeyboardAnimationCurveUserInfoKey = 0; UIKeyboardAnimationDurationUserInfoKey = 0.300000011920929; UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {768, 359}}; UIKeyboardCenterBeginUserInfoKey = NSPoint: {384, 1203.5}; UIKeyboardCenterEndUserInfoKey = NSPoint: {384, 844.5}; UIKeyboardFrameBeginUserInfoKey = NSRect: {{0, 1024}, {768, 359}}; UIKeyboardFrameEndUserInfoKey = NSRect: {{0, 665}, {768, 359}}; }}
Wenn Sie in die untere rechte Ecke der Standardtastatur tippen, um sie zu schließen, sehen Sie die entsprechenden Informationen für die UIKeyboardWillHideNotication. In beiden Fällen besteht die Notifikation aus dem Namen und einem userInfo-Dictionary. userInfo sagt uns, wie lange es dauern wird, die Tastatur animiert auf den Bildschirm oder vom Bildschirm weg zu bringen. Es enthält außerdem Informationen zur Geometrie der Tastatur, die wir nutzen können, um unseren Text-View mit der Tastatur zu animieren.
5.6
Den Text-View animieren Wir werden die Daten aus dem userInfo-Dictionary nutzen, um den Text-View synchron mit der Tastatur und unserer Zusatztastatur zu animieren und in der Größe anzupassen.
4 Diese Meldungen werden in die Konsole geschrieben, auf die Sie über das XcodeMenüelement Run → Console zugreifen.
Den Text-View animieren W 111 Wenn wir die Größe des Text-View anpassen, müssen wir uns um zwei Dinge kümmern. Wir müssen sicherstellen, dass die Dauer der TextView-Animation der Zeit entspricht, die die Animation läuft, mit der die Tastatur auf den Bildschirm gelangt oder von ihm runter. Da wir gerade dabei sind, können wir auch sichern, dass wir die gleiche Animationskurve wie die Tastatur nutzen, damit die Animationen einander genau entsprechen. TextInput/Feelings6/Classes/FeelingsViewController.m
-(void) matchAnimationTo:(NSDictionary *) userInfo { [UIView setAnimationDuration: [[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]]; [UIView setAnimationCurve: [[userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]]; }
Außerdem müssen wir den Text-View um die Höhe der beiden Tastaturen anpassen. Zunächst berechnen wir die Endhöhe der Tastatur: TextInput/Feelings6/Classes/FeelingsViewController.m
-(CGFloat) keyboardEndingFrameHeight:(NSDictionary *) userInfo { CGRect keyboardEndingUncorrectedFrame = [[ userInfo objectForKey:UIKeyboardFrameEndUserInfoKey ] CGRectValue]; CGRect keyboardEndingFrame = [self.view convertRect:keyboardEndingUncorrectedFrame fromView:nil]; return keyboardEndingFrame.size.height; }
Beachten Sie, dass wir die Methode convertRect:fromView: nutzen mussten, um das Endrechteck der Tastatur umzuwandeln, das wir aus dem Eintrag UIKeyboard-FrameEndUserInfoKey entnommen haben. Da das Rechteck, das wir erhalten, die Ausrichtung des Bildschirms nicht berücksichtigt, müssen wir convertRect:fromView: aufrufen, um Breite und Höhe auszutauschen, wenn sich das Gerät im LandscapeModus befindet. Beachten Sie, dass wir anhand der Endhöhe der Tastatur die neue Rahmengröße für unseren Text-View berechnen können. Erscheint die Tastatur, mindern wir die Höhe des Text-View um die Höhe der Tastatur. Verschwindet die Tastatur, vergrößern wir die Höhe des Text-View um die Höhe der Tastatur.
112 X Kapitel 5: Angepasste Tastaturen TextInput/Feelings6/Classes/FeelingsViewController.m
-(CGRect) adjustFrameHeightBy:(CGFloat) change multipliedBy:(NSInteger) direction { return CGRectMake(20, 20, self.textView.frame.size.width, self.textView.frame.size.height + change * direction); }
Nutzen Sie die folgenden Hilfsmethoden, damit Sie in den Methoden keyboardWillAppear: und keyboardWillDisappear: das richtige Verhalten erzeugen. TextInput/Feelings6/Classes/FeelingsViewController.m
-(void)keyboardWillAppear:(NSNotification *)notification { [UIView beginAnimations:nil context:NULL]; [self matchAnimationTo:[notification userInfo]]; self.textView.frame = [self adjustFrameHeightBy:[self keyboardEndingFrameHeight: [notification userInfo]] multipliedBy:-1]; [UIView commitAnimations]; } -(void)keyboardWillDisappear:(NSNotification *) notification { [UIView beginAnimations:nil context:NULL]; [self matchAnimationTo:[notification userInfo]]; self.textView.frame = [self adjustFrameHeightBy:[self keyboardEndingFrameHeight: [notification userInfo]] multipliedBy:1]; [UIView commitAnimations]; }
Erstellen Sie die App und führen Sie sie aus. Jetzt passt das Verhalten sowohl im Landscape- als auch im Portrait-Modus. Wenn die Tastatur sichtbar ist, sollte der Text-View beispielsweise so aussehen:
Zusammenfassung W 113
Prüfen Sie, ob Sie das Gerät bei sichtbarer Tastatur drehen können und ob Sie die Tastatur schließen können, ohne dass sich das auf die Funktionsfähigkeit unserer Animationen auswirkt. Das Erstellen angepasster Tastaturen und Tastaturerweiterungen ist einfach, aber es ist wichtig, sich dabei mit Details wie diesen zu befassen.
5.7
Zusammenfassung In diesem Kapitel haben Sie gesehen, wie einfach man angepasste Tastaturen erstellen oder zu den von Apple gestellten Tastaturen eine Erweiterung hinzufügen kann – oder beides. Setzen Sie angepasste Tastaturen nur in Ausnahmefällen ein. Ihre Nutzer sind mit den Standardtasturen sehr vertraut. Ändern Sie die Tastatur also nur, wenn es einen überzeugenden Grund für den Einsatz einer nicht standardmäßigen Tastatur gibt. Ihre angepassten Tastaturen bedeuten eine zusätzliche kognitive Last. Sie müssen sicherstellen, dass das, was Sie bieten, diesen Aufwand wert ist.
114 X Kapitel 5: Angepasste Tastaturen Hier sehen Sie beispielsweise eine angepasste Tastatur aus Apples Numbers-App:
Diese Nicht-Standardtastatur erleichtert und beschleunigt die Dateneingabe für das ausgewählte Feld. Wenn Ihre Anwender Text eingeben müssen, sollten Sie immer erst überlegen, welche Standardtastatur Sie nutzen können. Anschließend denken Sie darüber nach, ob und wie Sie dem Anwender die Aufgabe erleichtern können, indem Sie eine angepasste Tastatur anbieten. Denken Sie daran: Das Ziel ist, App und Gerät zum Verschwinden zu bringen und dem Anwender zu ermöglichen, sich allein auf das zu konzentrieren, was er erreichen oder womit er sich vergnügen will. Wie Sie in diesem Kapitel gesehen haben, müssen Sie nicht viel tun, wenn Sie die Texteingabe anpassen wollen. Sie müssen nur die Eigenschaft inputView oder inputAccessoryView setzen, entsprechend konfigurieren, alle notwendigen Aktionen abwickeln und sicherstellen, dass Sie die Elemente um die Tastatur richtig bewegen.
Kapitel 6
Zeichnen Diese neuen Geräte von Apple halten Designer auf Trab. Sie besitzen vielleicht schon eine Sammlung von Bildern, die Sie in einer iPhoneApp hätten nutzen können, und dann betritt das iPad die Bühne, und Sie begeben sich erneut an den Zeichentisch. Ein Bild, das auf dem iPhone gut aussieht, könnte auf dem iPad zu klein wirken, also fangen Sie vielleicht damit an, eine iPhone- und eine iPad-Version Ihrer Bilder zu erstellen, wie wir es bei den Symbolen im ersten Kapitel gemacht haben. Aber das ist noch nicht das Ende der Geschichte. Das iPhone 4 hat einen neuen Bildschirm mit der doppelten Anzahl Pixel in beide Richtungen. Wenn Sie Ihre Grafiken nicht für dieses neue Gerät überarbeiten, sehen sie ebenso unscharf aus, wie im ersten Kapitel, als wir den Kompatibilitätsmodus nutzten, um unsere iPhone-App auf dem iPad auszuführen. Wenn Sie für iPhone und iPad schreiben, benötigen Sie also drei verschiedene Versionen aller Bilder, da es drei verschiedene Bildschirmmaße und -auflösungen gibt – und Sie wissen genau, dass das über kurz oder lang noch mehr werden! Sie können Ihren Designer um drei verschiedene Versionen der Grafiken bitten, und in vielen Fällen wird das die richtige Lösung sein. Aber häufig können Sie die paar Figuren, die Sie benötigen, programmatisch erstellen. Es kann mühselig, es kann qualvoll sein, aber für einfache Beispiele ist es eventuell die richtige Wahl. In diesem Kapitel beginnen wir mit den C-basierten Zeichen-APIs, die es schon seit Jahren auf Mac und iPhone gibt. Dann werden wir die neu eingeführte Cocoa-Klasse UIBezierPath nutzen, die fast vollständig ihrem Mac OS X-Cousin, der Klasse NSBezierPath, entspricht.
116 X Kapitel 6: Zeichnen In beiden Fällen ist die Strategie für das Zeichnen die gleiche. Sie erstellen einen Kontext und einen Pfad, den Sie mit einem Strich und/oder einer Füllung versehen. Die Einzelheiten lassen sich oft leichter zeichnen, wenn man das Objekt bei (0,0) zeichnet und es dann später an den Ort verschiebt, an dem es auf dem Bildschirm erscheinen soll. Wir beginnen dieses Kapitel mit einer C-basierten Version, in der wir ein gelbes Dreieck in die Mitte des Bildschirms zeichnen. Dann überführen wir dieses Beispiel in den vertrauteren Cocoa-Rahmen und fügen unserer Zeichnung weitere Komponenten hinzu, Dinge wie Ovale, Rechtecke und Kreissegmente. Schließlich schauen wir uns zwei unterschiedliche Formen von Bézierkurven an und geben das Ergebnis als PDF aus.1 Auch mit den neuen und verbesserten Klassen und Methoden, die für das iPad eingeführt wurden, ist das Zeichnen mit Code immer noch eine mühevolle Angelegenheit. Bei einem so simplen Beispiel wie dem in diesem Kapitel sind Sie mit drei Versionen eines PNG besser bedient, die Sie in Photoshop oder Omnigraffle erstellen und in Ihrer Anwendung nutzen. Dieses Beispiel zielt weniger auf das Ergebnis ab, sondern vielmehr auf die Techniken, die Sie auf dem Weg zum Ziel kennenlernen.
6.1
Zeichnen mit Core Graphics Erstellen Sie ein neues View-basiertes iPad-Projekt namens Bezier. Fügen Sie ihm eine neue Unterklasse von UIView hinzu, der Sie den Namen BezierView geben. Vergessen Sie nicht, den Typ des UIView in der BezierViewController -Nib in BezierView zu ändern. In der Klasse BezierView werden wir sämtliche Zeichenoperationen abwickeln. Wir könnten unseren gesamten Zeichencode in die Methode drawRect: stecken, aber wir wollen die Pfade des wohlorganisierten Programmierens nicht verlassen und werden große Teile der eigentlichen Arbeit in andere Methoden auslagern. Die Methode drawRect: bietet einen Überblick über die zu leistende Arbeit. Drawing/Bezier1/Classes/BezierView.m
-(void)drawRect:(CGRect)rect { CGMutablePathRef triangle = [self triangle]; CGContextRef ctx = UIGraphicsGetCurrentContext(); 1 Umfassende Informationen zu diesem Thema finden Sie in Programming with Quartz, 2D, and PDF Graphics in Mac OS X [GL06] und Quartz 2D Graphics for Mac OS X Developers [Tho06].
Zeichnen mit Core Graphics [self centerContext:ctx]; [self fill:triangle withColor:[UIColor yellowColor] inContext:ctx]; [self stroke:triangle withColor:[UIColor blackColor] width:20.0 inContext:ctx]; [self restoreContext:ctx]; CGPathRelease(triangle); }
Wenn wir fertig sind, wollen wir etwas der folgenden Gestalt sehen:
Sie sehen, dass wir zuerst den Pfad für das Dreieck erstellen müssen und dass wir den aktuellen Grafikkontext abrufen und speichern müssen. CGMutablePath und CGContext sind die Objekte, auf denen der Zeichenprozess basiert. Wir erstellen das Dreieck, bewegen es zur Bildschirmmitte, füllen es gelb, geben ihm einen 20 Pixel breiten schwarzen Rand und räumen dann hinter uns auf. Schauen wir uns jetzt die einzelnen Schritte genauer an.
W
117
118 X Kapitel 6: Zeichnen
Den Dreieckspfad erstellen Wir erstellen und speichern unseren Dreieckspfad über diesen Aufruf in der Methode drawRect: CGMutablePathRef triangle = [self triangle];
Die Methode triangle erstellt einen neuen, veränderbaren Pfad und bewegt zunächst den Zeichencursor zum Scheitelpunkt des Dreiecks. Dann wird dem Pfad eine Linie hinzugefügt (deswegen muss es ein veränderlicher Pfad sein), die den rechten Schenkel des Dreiecks darstellt. Anschließend wird eine weitere Linie eingefügt, die die Basis des Dreiecks bildet. Schließlich wird das Dreieck mit einer weiteren Linie geschlossen. Drawing/Bezier1/Classes/BezierView.m
-(CGMutablePathRef) triangle { CGMutablePathRef path = CGPathCreateMutable(); CGPathMoveToPoint(path, NULL, 0,-173); CGPathAddLineToPoint(path, NULL, 200,173); CGPathAddLineToPoint(path, NULL,-200,173); CGPathCloseSubpath(path); return path; }
Diese Operationen werden mit Aufrufen von C-Funktionen bewirkt. Beachten Sie, dass die Funktionen CGPathMoveToPoint(), CGPathAddLineToPoint() und CGPathClose-Subpath() den Pfad path als Parameter erwarten.
Den Pfad verschieben Es ist leichter, wenn Sie den Pfad mit dem Mittelpunkt bei (0,0) zeichnen und ihn später verschieben. Das vereinfacht die Berechnungen für den Pfad und sorgt dafür, dass Sie Komponenten später relativ problemlos wiederverwenden können. Beispielsweise hätten wir ganz einfach eine App erstellen können, die das gelbe Dreieck jeweils an der Stelle zeichnet, an der der Benutzer den Bildschirm berührt. Wir würden jeweils einfach das gleiche Dreieck zeichnen und es dann an den Ort der Berührung verschieben. Der Aufruf zum Verschieben des Pfads ist in folgender Zeile der Methode drawRect: enthalten: [self centerContext:ctx];
Zeichnen mit Core Graphics Nach Fertigstellung der Zeichnung folgt ein entsprechender Aufruf von restoreContext:, mit dem wir hinter uns aufräumen. An diesen beiden Methoden ist nicht viel dran: Drawing/Bezier1/Classes/BezierView.m
-(void) centerContext:(CGContextRef) ctx { CGPoint center = [self convertPoint:self.center fromView:nil]; CGContextSaveGState(ctx); CGContextTranslateCTM(ctx, center.x, center.y); } -(void) restoreContext:(CGContextRef) ctx { CGContextRestoreGState(ctx); }
In centerContext: berechnen wir den Mittelpunkt des View und berücksichtigen damit eine eventuelle Drehung des Geräts. Bevor wir den Kontext verschieben, speichern wir den aktuellen Core GraphicsZustand. Dann verschieben wir den Kontext in den Mittelpunkt des View. Das heißt, dass sich das Dreieck jetzt im Mittelpunkt des View befindet und nicht in der linken oberen Ecke. Sobald das Zeichnen abgeschlossen ist, setzen wir den Status auf den gespeicherten Wert zurück.
Strich und Füllung Jetzt haben wir die meiste Arbeit bereits erledigt. Wir haben das Dreieck erstellt und an den Mittelpunkt des Bildschirms verschoben. Aber leider sieht man noch nichts. Mit folgenden Aufrufen in der Methode drawRect: ergänzen wir die Farben: [self fill:triangle withColor:[UIColor yellowColor] inContext:ctx]; [self stroke:triangle withColor:[UIColor blackColor] width:20.0 inContext:ctx];
Es ist nicht überraschend, dass Sie den Pfad an die Methoden übergeben müssen, die sich um Strich und Füllung des Dreiecks kümmern. Was überrascht, ist, dass Sie auch den Kontext übergeben müssen. Wenn Sie sich die Implementierungen dieser Methoden ansehen, werden Sie feststellen, dass der Kontext als Parameter für alle enthaltenen Funktionsaufrufe benötigt wird.
W
119
120 X Kapitel 6: Zeichnen Drawing/Bezier1/Classes/BezierView.m
-(void) fill: (CGMutablePathRef) path withColor:(UIColor *) color inContext: (CGContextRef) ctx { CGContextSetFillColorWithColor(ctx, color.CGColor); CGContextAddPath(ctx, path); CGContextFillPath(ctx); } -(void) stroke:(CGMutablePathRef) path withColor:(UIColor *) color width:(CGFloat) width inContext:(CGContextRef) ctx { CGContextSetStrokeColorWithColor(ctx, color.CGColor); CGContextSetLineWidth(ctx, width); CGContextAddPath(ctx, path); CGContextStrokePath(ctx); }
In beiden Fällen setzen wir die zu verwendende Farbe, fügen dem veränderten Kontext den Pfad hinzu und lassen dann Füllung bzw. Strich ausführen. Endlich gibt es etwas zu sehen. Angezeigt werden sollte jetzt ein großes gelbes Dreieck mit einem breiten schwarzen Rand im Zentrum des Bildschirms. Im nächsten Abschnitt werden wir dieses Beispiel unter Verwendung der Klasse UIBezierPath umarbeiten. Eigentlich gibt es keinen vernünftigen Grund dafür. Die Klasse UIBezierPath bietet eine Reihe von Funktionen, die sie recht attraktiv machen, aber wenn Sie bloß Pfade aus Linienelementen und einfachen Kurven aufbauen, können Sie die in diesem Abschnitt beschriebenen C-basierten APIs nutzen.
6.2
Die Cocoa-APIs nutzen Sie können schon an der Methode drawRect: sehen, dass unsere Implementierung einfacher ist als in der Cocoa-Version. Am überraschendsten ist, dass wir den Grafikkontext nicht nutzen müssen. Wir mussten uns keine Referenz darauf besorgen und diese an die Methoden übergeben, die sich um Strich und Füllung für das Dreieck kümmern. Drawing/Bezier2/Classes/BezierView.m
-(void)drawRect:(CGRect)rect { UIBezierPath *triangle = [[self triangle] retain]; [self centerPath: triangle]; [self fill:triangle withColor:[UIColor yellowColor]]; [self stroke:triangle withColor:[UIColor blackColor] width:20.0]; [triangle release]; }
Die Cocoa-APIs nutzen
W
121
Da wir jetzt eine Cocoa-Implementierung haben, müssen wir die Referenzzählungsregeln in diesem Rahmen befolgen und deswegen ein retain ergänzen, damit unser Dreieck erhalten bleibt, wenn es gezeichnet und verschoben wird. Schauen wir uns die weiteren Unterschiede in dieser Implementierung in den einzelnen Phasen an.
Den Dreieckspfad erstellen Die Übersetzung der C-Funktionsaufrufe in die Objective-C-Methodenaufrufe ist recht einfach. Folgendes CGPathMoveToPoint(path, NULL, 0,-173);
wird beispielsweise zu: [path moveToPoint:CGPointMake(0,-173)];
Sie müssen die Variable path nicht mehr als Parameter übergeben, da Sie ihr selbst die Nachricht schicken. Nehmen Sie im Rest ähnliche Änderungen vor, um die Cocoa-Version der Methode triangle zu erstellen. Drawing/Bezier2/Classes/BezierView.m
-(UIBezierPath *) triangle { UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:CGPointMake(0,-173)]; [path addLineToPoint:CGPointMake(200,173)]; [path addLineToPoint:CGPointMake(-200,173)]; [path closePath]; return path; }
Alle Methoden sind kaum mehr als schlanke Wrapper um die entsprechende C-Funktion, und doch fühlt sich dieses Verfahren sauberer an. Da das natürlich im Auge des Betrachters liegt, haben wir Ihnen beide Versionen vorgestellt.2
Den Pfad verschieben Diesmal verschieben wir den Pfad, nicht den Kontext: Drawing/Bezier2/Classes/BezierView.m
-(void) centerPath:(UIBezierPath *) path { CGPoint center = [self convertPoint:self.center fromView:nil]; [path applyTransform:CGAffineTransformMakeTranslation(center.x, center.y)]; } 2 Sie können die beiden Verfahren mischen. Es ist allerdings nicht ganz einfach, die Informationen zwischen den beiden Welten synchron zu halten. Um die C- und CocoaAPIs zu vermischen, sollten Sie einen Blick in Apples Drawing and Printing Guide for iOS [App10d] werfen.
122 X Kapitel 6: Zeichnen Wie zuvor berechnen wir den Mittelpunkt, wenden diesmal die Transformation aber auf den Pfad selbst an. Hier nutzen wir eine einfache Translation, um den Mittelpunkt des Dreiecks zum Mittelpunkt des Bildschirms zu verschieben. Zuvor mussten wir an dieser Stelle den Kontext wiederherstellen. Hier tun wir das nicht. Wollten wir hier mehrere Dreiecke an unterschiedlichen Orten zeichnen, müssten wir eine Kopie des Dreiecks erstellen, diese verschieben und mit Strich und Füllung versehen. Dazu würden wir eine Änderung folgender Form an der Methode drawRect: vornehmen: -(void)drawRect:(CGRect)rect { UIBezierPath *triangleTemplate = [self triangle]; UIBezierPath *triangle = [triangleTemplate copy]; [self centerPath: triangle]; [self fill:triangle withColor:[UIColor yellowColor]]; [self stroke:triangle withColor:[UIColor blackColor] width:20.0]; X [triangle release]; X [triangleTemplate release]; } X X
Strich und Füllung Sie müssen nicht explizit den Kontext nutzen, wenn Sie die Farben setzen oder Strich und Füllung ausführen. Der aktuelle Kontext wird auf beiden Seiten eines Aufrufs von stroke oder fill gespeichert bzw. wiederhergestellt: Drawing/Bezier2/Classes/BezierView.m
-(void) fill: (UIBezierPath *) path withColor:(UIColor *) color { [color setFill]; [path fill]; } -(void) stroke:(UIBezierPath *) path withColor:(UIColor *) color width:(CGFloat) width { [color setStroke]; path.lineWidth = width; [path stroke]; }
Mit diesem Schritt haben viele Leute Probleme. Warum man die fillNachricht an path sendet, ist recht klar, aber wie man die Strich- und Füllfarben setzt, scheint seltsam. Hier senden wir einer Farbe eine Nachricht, die die Strich- oder die Füllfarbe ist. Das ist, als würden wir Folgendes sagen: “Gelber Stift, mache dich zum Füllen bereit.” Dann
Kreise und Rechtecke zeichnen sagen wir: “Lass das Füllen beginnen!”, und der gelbe Stift weiß, dass das seine Aufgabe ist. Sollten die Strich- und Füllfarben nicht Eigenschaften der UIBezierPath-Instanz sein? Wahrscheinlich. Aber sie sind es nicht, und deswegen müssen Sie die Sache auf diese Weise machen. Wenn das alles wäre, was man mit UIBezierPaths machen könnte, würden Sie sich wahrscheinlich fragen, was der ganze Betrieb soll. Glücklicherweise können wir sie auch nutzen, um Rechtecke, Kreise, Rechtecke mit abgerundeten Ecken und, wie Sie sicher vermutet haben, Bézierkurven zeichnen. Das werden wir uns jetzt ansehen.
6.3
Kreise und Rechtecke zeichnen Fügen wir in die Mitte des Dreiecks ein Ausrufezeichen ein. Zunächst wird unser Ausrufezeichen aus einem Rechteck für die lange obere Linie und einem Punkt unten bestehen.
drawRect: müssen Sie dazu nur um die im Folgenden hervorgehobenen Zeilen erweitern, mit denen das Ausrufezeichen erstellt, verschoben und schwarz gefüllt wird: Drawing/Bezier3/Classes/BezierView.m
X X X X
-(void)drawRect:(CGRect)rect { UIBezierPath *triangle = [[self triangle] retain]; [self centerPath: triangle]; [self fill:triangle withColor:[UIColor yellowColor]]; [self stroke:triangle withColor:[UIColor blackColor] width:20.0]; UIBezierPath *exclamationPoint = [[self exclamationPoint] retain]; [self centerPath:exclamationPoint]; [self fill:exclamationPoint withColor:[UIColor blackColor]]; [exclamationPoint release]; [triangle release]; }
W
123
124 X Kapitel 6: Zeichnen Ein Ausrufezeichen besteht aus zwei Teilen: dem Strich und dem Punkt. Nutzen Sie die Methode appendPath:, um das Ausrufezeichen aus diesen beiden Komponenten aufzubauen: Drawing/Bezier3/Classes/BezierView
-(UIBezierPath *) exclamationPoint { UIBezierPath *path = [UIBezierPath bezierPath]; [path appendPath:[self top]]; [path appendPath:[self dot]]; return path; }
Der Strich ist ein mit der Methode bezierPathWithRect: erstelltes Rechteck, der Punkt ein mit der Methode bezierPathWithOvalInRect: erstellter Kreis: Drawing/Bezier3/Classes/BezierView.m
-(UIBezierPath *) top { return [UIBezierPath bezierPathWithRect:CGRectMake(-20, -70, 40, 140)]; } -(UIBezierPath *) dot { return [UIBezierPath bezierPathWithOvalInRect:CGRectMake(-25, 95, 50, 50)]; }
Ab hier bis zum Ende des Kapitels werden wir kleine Änderungen vornehmen, um das Aussehen des Ausrufezeichens zu verändern. Beispielsweise können wir die Ecken glätten, indem wir das Rechteck in ein Rechteck mit abgerundeten Ecken ändern: Drawing/Bezier4/Classes/BezierView.m
-(UIBezierPath *) top { return [UIBezierPath bezierPathWithRoundedRect:CGRectMake(-20, -70, 40, 140) cornerRadius:8.0]; }
Wir können die Ecken noch stärker runden, indem wir für die Ecken einen Radius nutzen, der der halben Breite des Rechtecks entspricht:
Unregelmäßige Pfade Drawing/Bezier5/Classes/BezierView.m
-(UIBezierPath *) top { return [UIBezierPath bezierPathWithRoundedRect:CGRectMake(-20, -70, 40, 140) cornerRadius:20.0]; }
Jetzt sieht es aus, als hätten wir an beide Enden des Rechtecks einen Halbkreis angefügt:
Als Nächstes werden wir den Pfad verändern und den Pfaden Kreisbögen hinzufügen.
6.4
Unregelmäßige Pfade Wir werden den Pfad für den Strich des Ausrufezeichens so verändern, dass er spitz nach unten zuläuft. Der eigentliche Zweck aber ist, deutlich zu zeigen, dass er nicht auf einem Rechteck basiert:
Drawing/Bezier6/Classes/BezierView.m
-(UIBezierPath *) top { UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:CGPointMake(-30, -70)]; [path addLineToPoint:CGPointMake(30, -70)]; [path addLineToPoint:CGPointMake(5, 70)]; [path addLineToPoint:CGPointMake(-5, 70)]; [path closePath]; return path; }
W
125
126 X Kapitel 6: Zeichnen Sie haben bereits gesehen, wie man Pfade auf Basis anderer Pfade erstellt und wie man Pfade aus Linien und Kurven aufbaut. Die Bestandteile sind einfach, aber die Kombinationen können mächtig und komplex sein. Nehmen Sie beispielsweise die hervorgehobenen Änderungen vor, um die Pfadsegmente oben und unten am Strich des Ausrufezeichen durch Halbkreise zu ersetzen.
Drawing/Bezier7/Classes/BezierView.m
X X X X X
X X X X X X
-(UIBezierPath *) top { UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:CGPointMake(-30, -70)]; [path appendPath:[UIBezierPath bezierPathWithArcCenter:CGPointMake(0, -70) radius:30 startAngle:M_PI endAngle:2*M_PI clockwise:YES]]; [path addLineToPoint:CGPointMake(30, -70)]; [path addLineToPoint:CGPointMake(5, 70)]; [path appendPath:[UIBezierPath bezierPathWithArcCenter:CGPointMake(0, 70) radius:5 startAngle:0 endAngle:M_PI clockwise:YES]]; [path addLineToPoint:CGPointMake(-30, -70)]; return path; }
Wir geben für jeden Kreisbogen den Mittelpunkt und den Radius sowie den Anfangs- und Endwinkel in Radiant an. Außerdem müssen wir anzeigen, ob er im oder entgegen dem Uhrzeigersinn verläuft. Hier laufen beide Kreisbogen im Uhrzeigersinn. Das liegt nur zum Teil daran, dass wir die Pfadkomponenten im Uhrzeigersinn durchlaufen. Würden wir die Kreisbogen gegen die Uhr durchlaufen, hätten wir an den Enden Senken statt Erhebungen. Wenn Sie den Pfad in umgekehrter Richtung zeichneten, müssten Sie auch die Kreisbogen gegen die Uhr verlaufen lassen, damit Sie Erhebungen statt Senken haben.
Bézierkurven nutzen
Obwohl die Kurven scheinbar glatt verlaufen, ist das nicht der Fall. An den vier Punkten, an denen die geraden Linien und die Kreisbogen zusammentreffen, gibt es kaum merkliche Ecken. Hier sind diese Ecken nicht erkennbar. Wir könnten sie deswegen einfach ignorieren. Denken Sie allerdings daran, dass der Zweck dieses Kapitels war, Ihnen nützliche Techniken nahezubringen. Es wird Zeit, dass Sie Bekanntschaft mit den Methoden machen, die der Klasse UIBezierPath ihren Namen geben.
6.5
Bézierkurven nutzen Mit der Klasse UIBezierPath können Sie zwei Arten von Bézierkurven zeichnen. Die quadratische Kurve wird über zwei Endpunkte und einen Kontrollpunkt angegeben, der die Richtung der Tangente an den beiden Endpunkten definiert. Im Code nutzen wir die Methode addQuadCurveToPoint:controlPoint:. Wir geben nur zwei Punkte an: den Endpunkt und den Kontrollpunkt. Der Startpunkt entspricht dem aktuellen Wert der currentPoint-Eigenschaft des Pfads. Die resultierende Kurve ist eine quadratische Bézierkurve, die am Punkt currentPoint mit einer Richtung beginnt, die durch den von currentPoint und dem Kontrollpunkt definierten Vektor bestimmt wird. Die Kurve endet an dem Endpunkt, der als erster Methodenparameter übergeben wird. Die Richtung am Endpunkt wird durch den Vektor bestimmt, der von Kontrollpunkt und Endpunkt gebildet wird. Hier ersetzen wir den unteren Bogen durch eine quadratische Bézierkurve: Drawing/Bezier8/Classes/BezierView.m
-(UIBezierPath *) top { UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:CGPointMake(-30, -70)];
W
127
128 X Kapitel 6: Zeichnen [path appendPath:[UIBezierPath bezierPathWithArcCenter:CGPointMake(0, -70) radius:30 startAngle:M_PI endAngle:2*M_PI clockwise:YES]]; [path addLineToPoint:CGPointMake(30, -70)]; [path addLineToPoint:CGPointMake(5, 70)]; [path addQuadCurveToPoint:CGPointMake(-5, 70) controlPoint:CGPointMake(0, 98)]; [path addLineToPoint:CGPointMake(-30, -70)]; return path;
X X
}
Der Kontrollpunkt wurde so gewählt, dass er der Punkt ist, an dem sich die beiden Seiten des Ausrufezeichens begegnen würden, wenn man sie verlängerte. Für die obere Kurve des Ausrufezeichens können wir diesen Trick nicht nutzen, da die Seiten in diese Richtung auseinanderlaufen. Außerdem könnten wir die Kurve nicht mit einer quadratischen Bézierkurve aufbauen, da wir ansonsten an den Anschlusspunkten Ecken einfügen würden. Wir werden deswegen eine kubische Bézierkurve nutzen, da diese uns zwei Kontrollpunkte bietet: einen für die Tangentenrichtung der Kurve an jedem Endpunkt.3 Drawing/Bezier9/Classes/BezierView.m
-(UIBezierPath *) top { UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:CGPointMake(-30, -70)]; X [path addCurveToPoint:CGPointMake(30, -70) X controlPoint1:CGPointMake(-35, -98) X controlPoint2:CGPointMake(35, -98)] [path addLineToPoint:CGPointMake(30, -70)]; [path addLineToPoint:CGPointMake(5, 70)]; [path addQuadCurveToPoint:CGPointMake(-5, 70) controlPoint:CGPointMake(0, 98)]; [path addLineToPoint:CGPointMake(-30, -70)]; return path; }
Auch hier ist der Startpunkt wieder der currentPoint-Wert des Pfads, und die anfängliche Richtung wird erneut durch currentPoint und den ersten Kontrollpunkt bestimmt. Der Endpunkt ist wieder der erste Methodenparameter, und die Endrichtung wird durch den zweiten Kontrollpunkt und diesen Endpunkt bestimmt. Die resultierende Kurve schließt glatt an die beiden Seiten an.
3 Erläuterungen der beiden Typen von Bézierkurven und der Verwendung ihrer Endund Kontrollpunkte finden Sie in Apples Drawing and Printing Guide for iOS [App10d].
Unsere Zeichnung als PDF speichern W 129
Verschieben wir die beiden Kontrollpunkte etwas weiter nach außen: Drawing/Bezier10/Classes/BezierView.m
[path addCurveToPoint:CGPointMake(30, -70) controlPoint1:CGPointMake(-40, -126) controlPoint2:CGPointMake(40, -126)];
Sie sehen, dass die Länge der beiden Vektoren, die die Richtung beschreiben, auch das Verhalten der Bézierkurve ändert:
Sie können mit den Kontrollpunkten und den Seiten des Ausrufezeichens experimentieren, bis Sie genau das Aussehen haben, das Sie sich vorgestellt haben.
6.6
Unsere Zeichnung als PDF speichern Ziehen wir den Inhalt der Methode drawRect: heraus, damit wir die Befehle, mit denen wir auf den Bildschirm zeichnen, nutzen können, um ein PDF des Bilds zu erstellen: Drawing/Bezier11/Classes/BezierView.m
-(void) drawInCurrentContext { UIBezierPath *triangle = [[self triangle] retain]; [self centerPath: triangle]; [self fill:triangle withColor:[UIColor yellowColor]]; [self stroke:triangle withColor:[UIColor blackColor] width:20.0]; UIBezierPath *exclamationPoint = [[self exclamationPoint] retain]; [self centerPath:exclamationPoint]; [self fill:exclamationPoint withColor:[UIColor blackColor]];
130 X Kapitel 6: Zeichnen [exclamationPoint release]; [triangle release]; } -(void)drawRect:(CGRect)rect { [self drawInCurrentContext]; }
Wenn wir die App erstellen und ausführen, erhalten wir das gleiche Ergebnis wie zuvor, können jetzt aber den PDF-Kontext setzen und drawInCurrentContext aufrufen, um unser Warnzeichen in eine PDFDatei im Documents-Verzeichnis zu schreiben. Der erste Schritt ist, dass uns pathToWarningFile den Pfad liefert, in dem wir unser PDF mit der Methode applicationDocumentDirectory anlegen und speichern. Drawing/Bezier12/Classes/BezierView.m
-(NSString *)applicationDocumentsDirectory { return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject]; } -(NSString *) pathToWarningFile { return [[self applicationDocumentsDirectory] stringByAppendingPathComponent:@"Warning.pdf"]; }
Das gibt uns den Pfad der Datei Warning.pdf im Ordner Documents. Diesen übergeben wir als ersten Parameter an die Funktion UIGraphicsBeginPDFContextToFile(), die wir nutzen werden, um den aktuellen Kontext zur Erstellung der in der Datei zu speichernden PDF-Datei anzulegen und zu konfigurieren. Die beiden anderen Parameter sind die Seitendimensionen als CGRect und ein optionales NSDictionary mit Dokumentinformationen. Wir nutzen die View-Größe als Seitendimension. Sie können auch CGRectMake() nutzen, um ein CGRect zu erstellen, oder mit CGRectZero() eine Seite erstellen lassen, die 8,5×11 Zoll oder 612×792 Pixel groß ist. Anstatt das PDF direkt in einer Datei zu speichern, könnten Sie auch die Funktion UIGraphicsBeginPDFContextToData() nutzen, um das PDF in ein NSData-Objekt zu schreiben. Wir werden unmittelbar in eine Datei schreiben. Zunächst erstellen Sie eine neue PDF-Seite. Dann zeichnen Sie auf die PDF-Seite auf genau die gleiche Weise, wie Sie auf den Bildschirm zeichnen würden. Wir werden im gesamten Dokument die gleichen Seitendaten verwenden und können deswegen einfach mit der Funktion UIGraphicsBeginPDFPage() arbeiten. Sie können die Seitendaten über
Unsere Zeichnung als PDF speichern W 131 die Funktion UIGraphicsBeginPDFPageWithInfo() ändern, wenn Sie eine neue Seite erstellen. Benötigen Sie eine weitere Seite, fangen Sie mit einer dieser beiden Methoden eine neue an. Wenn Sie die letzte Seite fertig haben, beenden Sie den PDF-Kontext. In unserem einfachen Beispiel haben wir nur eine einzige Seite. Hier ist der Code zur Erstellung des PDFs: Drawing/Bezier12/Classes/drawrect.m
X X X X
-(void)drawRect:(CGRect)rect { [self drawInCurrentContext]; UIGraphicsBeginPDFContextToFile([selfpathToWarningFile],self.frame,nil); UIGraphicsBeginPDFPage(); [self drawInCurrentContext]; UIGraphicsEndPDFContext(); }
Zur Erstellung des PDFs haben wir genau den gleichen Code genutzt wie zum Zeichnen auf den Bildschirm. Der einzige Unterschied ist, dass wir zunächst den Kontext setzen und eine neue Seite erstellen und am Ende den Kontext schließen. Erstellen Sie die App und führen Sie sie im Simulator aus. Das PDF finden Sie, indem Sie dem Pfad der Anwendung folgen: Library/Application\ Support/iPhone\ Simulator/3.2/ Applications. Sie sehen eine Reihe nummerierter Verzeichnisse. Schauen Sie in alle hinein, bis Sie auf unsere Anwendung stoßen. Suchen Sie das Verzeichnis, das der Bezier -Anwendung entspricht, und schauen Sie ins Verzeichnis Documents. Dort sollten Sie die Datei Warning.pdf mit dem Warnsymbol finden.
Mehr Informationen zur Erstellung von PDFs finden Sie in Apples Drawing and Printing Guide for iOS [App10d].
132 X Kapitel 6: Zeichnen
6.7
Zusammenfassung Die Klasse UIBezierPath vereinfacht das Zeichnen und macht es mächtiger. Sie haben jederzeit die Möglichkeit, in Ihren Anwendungen Bilddateien zu erstellen und diese anstelle von Bildern zu nutzen, die vorab in Grafikprogrammen geschaffen wurden. Wenn der Zeichencode nicht übermäßig kompliziert ist, sollten Sie diesen Ansatz vorziehen.4
4 Eine Betrachtung dessen finden Sie in einem Blogeintrag von Jeff LaMarche in Reaktion auf einen Kommentar von Joe Conway. Beginnen Sie auf dieser Seite: http://iphonedevelopment.blogspot.com/2010/05/some-good-advice.html.
Kapitel 7
Der Movie Player Dem Wesen nach ist das iPad für den Medienkonsum konzipiert. Der große, in Händen zu haltende Bildschirm ist wie gemacht für die visuelle Darstellung, insbesondere von Videos. Beim iPhone kann man Videos nur im Vollbildmodus anzeigen, auf dem iPad hingegen können Videos unmittelbar in eine Anwendung integriert werden und die Benutzerinteraktion wird vollständig daran angepasst. Das SDK veröffentlicht den Zustand des Players, Sie können das Video also direkt steuern und die Wiedergabe mit dem Rest der Anwendung abstimmen. In den beiden folgenden Kapiteln werden wir uns den Movie Player ansehen: Wir werden ihn in eine Anwendung einbetten, Views darüberlagern, unsere eigene Wiedergabesteuerung und Wiedergabeliste erzeugen, Navigations-Thumbnails generieren und Inhalte streamen. Wenn Sie bereits mit Video Player-Frameworks gearbeitet haben, werden Sie wahrscheinlich erfahren haben, dass diese recht umständlich sind. Mit Apples neuem Framework nehmen wir im Handumdrehen einen ausgefeilten Player in Betrieb.
7.1
Einen View für Videos einrichten Auf den folgenden Seiten werden wir eine iPad-Anwendung dazu bringen, ein Video abzuspielen. Nachdem wir das erreicht haben, werden wir einen Schritt zurücktreten, uns die MPMoviePlayerController Architektur im Detail ansehen und unsere Implementierung erheblich interessanter gestalten. Im Ordner MoviePlayer1 haben wir bereits ein Projekt für Sie vorbereitet (wenn Sie gern alles selbst machen wollen, können Sie ein View-basiertes Projekt erstellen). Öffnen Sie das Projekt, finden Sie im Resources-Verzeichnis einen Film namens BigBuckBunny_
134 X Kapitel 7: Der Movie Player 640x360.m4v.1 Außerdem haben wir dem Frameworks-Verzeichnis bereits MediaPlayer.framework hinzugefügt.
Abbildung 7.1: Den Movie Player-View einrichten Zunächst werden wir uns darauf konzentrieren, den MPMoviePlayerController zu instantiieren und die Wiedergabe unseres Films zu steuern. Diese Klasse kümmert sich um alle Details der Wiedergabe und gibt uns einen View, den wir unserer View-Hierarchie hinzufügen können, um den Film anzuzeigen. MPMoviePlayerController kann Filme auch im Vollbildmodus anzeigen (das werden wir uns etwas weiter unten ansehen). Beginnen wir damit, dass wir die Nib-Datei MoviePlayerViewController.xib im Interface Builder öffnen und einen View einfügen, der das Video des MPMoviePlayerControllers aufnehmen kann.2 Wählen Sie 1 Je nachdem, welches Paket Sie haben, müssen Sie den Film selbst herunterladen (und dem Projekt hinzufügen). Sie finden ihn unter http://www.bigbuckbunny.org. Beachten Sie, dass der Film dem Copyright der Blender Foundation, http://www.bigbuckbunny.org, unterliegt.
Einen View für Videos einrichten W 135 den Main-View des MoviePlayerViewController und setzen Sie seine Hintergrundfarbe auf Schwarz. Ziehen Sie dann ein UIView-Objekt aus der Interface Builder Library in den View. Nutzen Sie den Size-Inspektor, um den neuen UIView zu konfigurieren, wie Sie es in Abbildung 7.1 auf der vorangehenden Seite sehen. Achten Sie darauf, dass Sie die Größe und den Autosizing-Constraint in Ihre Version übernehmen. Jetzt müssen Sie in der MoviePlayerViewController -Header-Datei ein Outlet auf den neuen View anlegen – so zum Beispiel: movieplayer/MoviePlayer1/Classes/MoviePlayerViewController.h
@interface MoviePlayerViewController : UIViewController { UIView *viewForMovie; } @property (nonatomic, retain) IBOutlet UIView *viewForMovie; @end
Hier haben wir einen UIView namens viewForMovie definiert und als Eigenschaft des Typs IBOutlet deklariert. Und vergessen Sie nicht, der Datei MoviePlayer1ViewController.m die synthesize-Direktive hinzuzufügen: movieplayer/MoviePlayer1/Classes/MoviePlayerViewController.m
@synthesize viewForMovie;
Bringen wir die Dinge jetzt zusammen, indem wir zur MoviePlayerViewController.xib-Nib-Datei im Interface Builder zurückkehren. Wir werden das gerade deklarierte IBOutlet (für viewForMovie) mit dem UIView verbinden. Control-klicken Sie dazu auf das File’s Owner -Objekt der Nib-Datei und ziehen Sie die Verbindung zum neuen View. Wählen Sie, wenn das Verbindungs-Pop-up aufspringt, viewForMovie, wie Sie es in Abbildung 7.2 sehen.
Der MPMoviePlayerController Nachdem wir die Arbeit im Interface Builder erledigt haben, können wir uns auf den Code konzentrieren. Wir werden eine Instanz der Klasse MPMoviePlayerController hinzufügen, indem wir sie in der MoviePlayer ViewController.h-Header-Datei deklarieren. Zunächst müssen wir die Header-Datei des Movie Player, MPMoviePlayerController.h, importie2 Nur zur Klarstellung: MoviePlayerViewController ist der Main-View-Controller, den wir für unsere Anwendung schreiben; die Klasse MPMoviePlayerController ist eine Klasse, die Teil von iOS (ab 3.2) ist, von Apple geschrieben wurde und die Wiedergabe von Videos steuert.
136 X Kapitel 7: Der Movie Player ren und dann eine Deklaration für den Player einfügen; außerdem deklarieren wir den Player als Eigenschaft. Und da wir gerade hier sind, können wir gleich auch eine Deklaration für eine Methode namens movieURL einfügen (zu der wir später kommen werden).
Abbildung 7.2: Den Movie-View anbinden movieplayer/MoviePlayer1/Classes/MoviePlayerViewController.h
#import X #import <MediaPlayer/MPMoviePlayerController.h>
@interface MoviePlayerViewController : UIViewController { UIView *viewForMovie; X MPMoviePlayerController *player; } @property (nonatomic, retain) IBOutlet UIView *viewForMovie; X @property (nonatomic, retain) MPMoviePlayerController *player; X -(NSURL *)movieURL; @end
Einen View für Videos einrichten W 137 Werfen wir jetzt einen Blick in die MoviePlayerViewController.m-Implementierungsdatei. Beginnen wir mit der Methode viewDidLoad:, in der wir den MPMoviePlayer-Controller instantiieren. Wir fügen den Code erst ein und gehen ihn anschließend durch: movieplayer/MoviePlayer1/Classes/MoviePlayerViewController.m
-(void)viewDidLoad { [super viewDidLoad]; self.player = [[MPMoviePlayerController alloc] init]; self.player.contentURL = [self movieURL]; }
Zunächst haben wir für den Movie-Controller alloc und init aufgerufen und ihn unserer player -Eigenschaft zugewiesen. Dann weisen wir dem Player die URL des Inhalts zu – das ist der Ort des Videoinhalts, den der Player abspielen soll. Es kann eine lokale Datei im Bundle oder eine netzwerkbasierte URL sein. Zunächst werden wir eine lokale Datei als Videoinhalt nutzen. Machen wir einen kleinen Umweg, um uns die Methode movieURL anzusehen, die eine dateibasierte URL auf den Videoinhalt im Bundle aufbaut: movieplayer/MoviePlayer1/Classes/MoviePlayerViewController.m
-(NSURL *)movieURL { NSBundle *bundle = [NSBundle mainBundle]; NSString *moviePath = [bundle pathForResource:@"BigBuckBunny_640x360" ofType:@"m4v"]; if (moviePath) { return [NSURL fileURLWithPath:moviePath]; } else { return nil; } }
Wenn Sie schon einmal eine URL auf ein Bild oder eine andere Ressource im Bundle aufgebaut haben, sollte Ihnen dieser Code vertraut sein: Wir beschaffen uns eine Referenz auf das Bundle, konstruieren eine URL auf das Video und packen diese in das NSURL-Objekt, das wir dann zurückliefern. Später in diesem Kapitel werden wir zu diesem Code zurückkehren und einige netzwerkbasierte URLs aufbauen, um Inhalte auf das iPad zu streamen. Beenden wir diesen kleinen Ausflug und wenden wir uns jetzt einem der wichtigsten Konzepte in diesem Kapitel zu: dem Einsatz der videoEigenschaft des MPMoviePlayerController. Wie bereits erwähnt, ver-
138 X Kapitel 7: Der Movie Player waltet der Player-Controller einen speziellen View, der als Subview einem beliebigen View hinzugefügt werden kann, um Videoinhalte anzuzeigen. Behandeln Sie diesen View mit Samthandschuhen – obwohl wir später in diesem Kapitel einige interessante Argumente dafür finden werden, dem View des Players unsere eigenen Subviews hinzuzufügen, ist das ein undurchsichtiges Objekt, das vom Player gesteuert wird, und auch als solches behandelt werden sollte. movieplayer/MoviePlayer1/Classes/MoviePlayerViewController.m
-(void)viewDidLoad { [super viewDidLoad]; self.player = [[MPMoviePlayerController alloc] init]; self.player.contentURL = [self movieURL]; self.player.view.frame = self.viewForMovie.bounds; self.player.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
X X X X X X X
[self.viewForMovie addSubview:player.view]; [self.player play]; }
Der Zugriff auf den Video-View des MPMoviePlayerController ist leicht; wir greifen auf die view-Eigenschaft zu wie auf jede andere Eigenschaft, und genau das tun wir auch in diesem Code. Nachdem wir eine Referenz auf den View haben, setzen wir zuerst zwei Eigenschaften: den Frame des View und einige Masken, die steuern, wie die Größe des Views geändert werden kann. Der Frame steuert, wie die Frames anderer Views auch, seine Größe auf dem Bildschirm. Hier erhalten wir den Frame vom movieForView-Objekt. Die Masken setzen wir so, dass der View nach Bedarf in der Breite oder Höhe angepasst wird. Nun zum interessanten Teil: Wir nehmen den View des Player-Controllers und fügen ihn als Subview dem View viewForMovie hinzu, den wir zuvor im Interface Builder erstellt haben. Ist das erledigt, senden wir dem Player die play-Nachricht, um die Wiedergabe zu starten. Wenn Sie jetzt auf „Build and Run“ klicken, sollten das Video im UIView abgespielt werden. Berühren Sie den View, werden einige Steuerelemente angezeigt, unter anderem ein Schalter für den Vollbildmodus. Wird das Gerät gedreht, sollte die Größe entsprechend angepasst werden.
Einblick in den Player
Das große Ganze oder was gerade geschah Schauen wir uns an, was da gerade passierte – beim ersten Mal kann das etwas verwirrend sein. Auch wenn es etwas kompliziert erscheint, ist es eigentlich ziemlich einfach, wenn man einmal alle beteiligten Views und Controller hinter sich gelassen hat. Zunächst haben wir ein MPMoviePlayerController -Objekt erstellt, das als Controller der Anzeige von Videoinhalten dient. Die entsprechenden Details werden wir uns gleich ansehen, wichtig ist, dass wir diesen Controller nutzen können, um alle Aspekte der Videowiedergabe zu verwalten, unter anderem auch das Starten, Stoppen, Anhalten und andere Aktionen. Außerdem veröffentlicht dieser Controller eine eingebaute Eigenschaft namens view, auf die wir uns eine Referenz verschaffen können und die wir dann unseren eigenen Views hinzufügen, um Videos anzuzeigen. Und genau das haben wir auch gemacht. Erinnern Sie sich: Zuerst haben wir im Interface Builder einen UIView erstellt, der als Landefläche für den Video-View dient. Dann, nachdem wir den MPMoviePlayerController erstellt haben, nahmen wir seinen internen View und fügten diesen als Subview unserem UIView hinzu. Das Ergebnis? Videowiedergabe unmittelbar in unsere Anwendung.
7.2
Einblick in den Player Nachdem wir den Player instantiiert und seinen View in unsere ViewHierarchie eingebunden haben, können wir uns vom Player ein paar Informationen zum Video und zu dem, was er macht, liefern lassen. Dazu werden wir der Anwendung einen Info-Button und ein Label hinzufügen und dann unseren MPMovie-PlayerController nutzen, um in dem Label ein paar Echtzeitinformationen anzeigen zu lassen, wenn der Button berührt wird. Kümmern wir uns zunächst um die Einrichtungsarbeiten, bevor wir uns dem MPMovie-PlayerController -Code zuwenden – öffnen Sie erneut die MoviePlayerViewController.xib-Nib-Datei im Interface Builder und ziehen Sie ein UILabel und einen UIButton auf den MainView. Positionieren Sie beide unten auf dem iPad-Bildschirm, wie Sie es in Abbildung 7.3 auf der folgenden Seite sehen. Außerdem müssen wir ein paar Dinge erledigen, um Button und Label ordentlich zu konfigurieren. Setzen Sie zunächst die Farbe des Labeltexts auf Weiß und entfernen Sie den Standardtext für das Label. Setzen Sie das Autosizing dann so, dass es relativ zum unteren Bildschirmrand positioniert bleibt. Schließlich müssen wir den Stil des
W
139
140 X Kapitel 7: Der Movie Player Buttons noch in Info Light anstelle des Standards Rounded Rect ändern und Autosizing ebenso setzen wie beim Label. Jetzt werden wir eine Aktion für den Info-Button mit dem Namen getInfo und ein Outlet für das Label mit dem Namen onScreenDisplayLabel erstellen. Beginnen wir mit den Ergänzungen in der Header-Datei MoviePlayerViewController.h: movieplayer/MoviePlayer2/Classes/MoviePlayerViewController.h
@interface MoviePlayerViewController : UIViewController { UIView *viewForMovie; MPMoviePlayerController *player; X UILabel *onScreenDisplayLabel; } @property (nonatomic, retain) IBOutlet UIView *viewForMovie; @property (nonatomic, retain) MPMoviePlayerController *player; X @property (nonatomic, retain) IBOutlet UILabel *onScreenDisplayLabel; -(NSURL *)movieURL; X -(IBAction)getInfo:(id)sender; @end
Abbildung 7.3: Label- und Button-Views positionieren und das Label konfigurieren
Einblick in den Player Haben Sie die MoviePlayerViewController.h-Header-Datei aktualisiert, können wir in den Interface Builder zurückkehren und das onScreenDisplayLabel-IBOutlet mit dem Label verbinden; danach setzen Sie die Aktion des Info-Buttons auf die Methode getInfo:. Jetzt können wir uns dem MPMoviePlayerController zuwenden. Weil er für die Steuerung der Wiedergabe verantwortlich ist, pflegt MPMoviePlayerController einige Eigenschaften, die die aktuelle Videoressource sowie einige Laufzeitinformationen zum Video wie Größe, Dauer und die aktuelle Wiedergabeposition beschreiben (um nur einige zu nennen). Wir werden uns in diesem Kapitel mit einigen dieser Eigenschaften befassen, aber wenn Sie eine vollständige Liste und weitere Details zu ihrer Bedeutung haben wollen, sollten Sie einen Blick in die MPMoviePlayerController Class Reference [App10f] werfen. Nutzen wir einige dieser Eigenschaften, um unserer getInfo:-Methode Gestalt zu geben: movieplayer/MoviePlayer2/Classes/MoviePlayerViewController.m
-(void) getInfo:(id)sender { MPMoviePlayerController *moviePlayer = self.player; float float float float float
width = moviePlayer.naturalSize.width; height = moviePlayer.naturalSize.height; playbackTime = moviePlayer.currentPlaybackTime; playbackRate = moviePlayer.currentPlaybackRate; duration = moviePlayer.duration;
onScreenDisplayLabel.text = [NSString stringWithFormat: @"[Zeit: %.1f von %.f Sekunden] \ [Wiedergabe: %.0fx] \ [Größe: %.0fx%.0f]", playbackTime, duration, playbackRate, width, height]; }
Hier besorgen wir uns eine Referenz auf den Movie-Controller und nutzen seine naturalSize-Eigenschaft, um die tatsächliche Größe der laufenden Videoressource abzurufen. Es kann sein, dass der Player das Video skaliert, damit es in einen anderen Frame passt (wenn das für Ihre Anwendung wichtig ist, können Sie den Frame des Players sowie seine scalingMode-Eigenschaften untersuchen), aber diese Eigenschaft gibt Ihnen die tatsächliche, native Größe des Videos.
W
141
142 X Kapitel 7: Der Movie Player Dann schauen wir uns die drei Eigenschaften an, die sich auf die Videowiedergabe beziehen: die aktuelle Wiedergabeposition (currentPlaybackTime), die absolute Dauer des Videos (duration) und die Wiedergabequote (currentPlaybackRate). Während die Dauer eine readonly-Eigenschaft ist, können die beiden anderen Eigenschaften gesetzt werden, um Wiedergabeposition bzw. -quote zu ändern. Sie sind ebenfalls Teil des MPMediaPlayback-Protokolls, das die Klasse MPMoviePlayerController implementiert, und geben uns eine standardisierte Schnittstelle für das Abspielen von Medien (mit den üblichen Methoden zum Abspielen, Anhalten, Beenden und Verschieben der Wiedergabeposition). Diese Methoden werden wir uns später in diesem Kapitel ansehen. Kompilieren Sie den neuen Code und führen Sie ihn aus. Tippen Sie mehrfach auf den Info-Button. Bei jedem Mal wird der Status der Eigenschaften aktualisiert. Ihnen sollte etwas in folgender Form angezeigt werden: [Zeit: 1.5 von 596 Sekunden] [Wiedergabe: 1x] [Größe: 640x359]
Nachdem wir diesen Code geschrieben haben, müssen wir gestehen, dass es einen Punkt gibt, den wir bislang verschwiegen haben: Viele dieser Eigenschaften sind erst bekannt, wenn das Video bis zu einem gewissen Punkt im Controller geladen ist. Da der Controller im Augenblick aus einer lokalen Datei gespeist wird, geschieht das ziemlich schnell. Aber Ressourcen können auch über das Netzwerk geladen werden, und dabei kann es zu großen Verzögerungen kommen. Es kann also passieren, dass auf den Info-Button getippt wird, bevor diese Eigenschaften bekannt sind. Wie also bringen wir in Erfahrung, ob die Eigenschaften des Players bereit sind? Das werden wir uns als Nächstes ansehen, wenn wir uns damit befassen, wie der Player einer Anwendung über Notifikation seinen Status mitteilt.
7.3
Benachrichtigt werden Die Klasse MPMoviePlayerController unterstützt einen umfangreichen Satz von Notifikationen, die Sie nutzen können, wenn Sie über den Status der Wiedergabe auf dem Laufenden bleiben wollen. Wie man wahrscheinlich erwartet, kann man sich über Wiedergabeereignisse wie Starten, Anhalten, Pause und Beenden benachrichtigen lassen. Zusätzlich kann man aber auch Notifikationen zu den folgenden Dingen erhalten:
Benachrichtigt werden 앫
Die Verfügbarkeit von Metadaten zum Video.
앫
Änderungen der Skalierung und Videoauflösung (einschließlich des Wechsels in oder aus dem Vollbildmodus).
앫
Statusänderungen in Bezug auf das Laden netzwerkbasierter Videos.
앫
Die Verfügbarkeit generierter Thumbnails (das werden wir uns später ansehen).
Wir werden diese Dinge nicht alle im Detail behandeln, wollen aber die Grundlagen der Arbeit mit den Notifikationen der Klasse MPMoviePlayerController behandeln und uns in einem späteren Abschnitt auch die Thumbnail-Fähigkeiten ansehen. Kehren wir für den Augenblick zum Problem des Zugriffs auf Eigenschaften (wie die Dauer des Videos) zurück, bevor der Player das Video so weit geladen hat, dass er über die entsprechenden Informationen verfügt. Mit Notifikationen können wir dieses Problem lösen, indem wir uns dafür registrieren, dass wir benachrichtigt werden, wenn die Metadaten verfügbar sind. Hier sind die auf Metadaten bezogenen Notifikationen, für die wir uns registrieren können: 앫 MPMovieDurationAvailableNotication
Wird gesendet, wenn die Dauer des Videos ermittelt wurde. 앫 MPMovieMediaTypesAvailableNotication
Wird gesendet, wenn die Audio- und Videotypen ermittelt wurden. 앫 MPMovieNaturalSizeAvailableNotication
Wird gesendet, wenn Breite und Höhe des Videos bekannt sind. Schreiben wir etwas Code, um die Notifikation MPMovieDurationAvailableNotication zu nutzen; als einfachen Test werden wir unmittelbar nachdem die Dauer verfügbar ist, unsere getInfo-Aktion aufrufen (und dabei indirekt voraussetzen, dass die anderen Metadaten ebenfalls verfügbar sind, wenn die Dauer verfügbar ist – diese Voraussetzung ist im Regelfall wohl erfüllt, dennoch sollten wir prüfen, ob die gelieferten Werte gültig sind). Beachten Sie, dass eine bessere Implementierung den Info-Button deaktivieren und erst, wenn die Metadaten verfügbar sind, aktivieren würde. Im Augenblick schauen wir uns aber nur die Grundlagen der Arbeit mit den Notifikationen des MPMoviePlayerController an – im nächsten Abschnitt werden wir zu einem komplexeren Beispiel der Arbeit mit Notifikationen zurückkehren.
W
143
144 X Kapitel 7: Der Movie Player
Notifikationen von unten Der Movie Player-Controller nutzt Cocoas Standard-Notifikationssystem. Hier ist eine kurze Auffrischung: Wenn Sie Notifikationen erhalten möchten, fügen Sie der jeweiligen Notifikation (wie MPMovieDurationAvailableNotication) ein Objekt als Beobachter hinzu und geben einen Selektor an, der aufgerufen wird, wenn es eine Benachrichtigung gibt. Zusätzlich können Sie ein optionales Datenobjekt angeben, das bei jeder Notifikation an das Beobachter-Objekt übergeben wird; wird kein Datenobjekt übergegeben (d.h., wird der Wert nil übergeben), wird das Objekt weitergereicht, das die Notifikation sendet. Hier ist der Code, mit dem Sie Ihren MoviePlayerViewController registrieren, damit er die MPMovieDurationAvailableNotication-Notifikation erhält. Wir werden diesen neuen Code in die Methode viewDidLoad einsetzen, damit die Registrierung erfolgt, bevor der Movie Player instantiiert wird. Wir setzen movieDurationAvailable: als Selektor für die Notifikation, damit die Methode aufgerufen wird, wenn eine MPMovieDurationAvailableNotication gesendet wird. movieplayer/MoviePlayer3/Classes/MoviePlayerViewController.m
-(void)viewDidLoad { [super viewDidLoad]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(movieDurationAvailable:) name:MPMovieDurationAvailableNotification object:nil];
X X X X X
self.player = [[MPMoviePlayerController alloc] init]; self.player.contentURL = [self movieURL]; self.player.view.frame = self.viewForMovie.bounds; self.player.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [self.viewForMovie addSubview:player.view]; [self.player play]; }
Mehr muss man nicht tun, um die Notifikation auf dem Movie Player einzurichten; wir müssen jetzt nur noch die Methode schreiben, die aufgerufen wird, wenn eine Notifikation eingeht: movieplayer/MoviePlayer3/Classes/MoviePlayerViewController.m
-(void) movieDurationAvailable:(NSNotification*)notification { [self getInfo:nil]; }
Eine Wiedergabeliste hinzufügen In dieser Methode rufen wir nur die Methode getInfo auf. Wenn Sie diesen Code erstellen und ausführen, sollte das Label gefüllt werden, sobald die Dauer verfügbar ist, so, als hätten Sie auf den Info-Button getippt. Eine etwas gebräuchlichere Verwendung dieser Notifikation wäre, die Dauer direkt in dieser Methode abzurufen. Wie wir das tun würden? So: MPMoviePlayerController *moviePlayer = [notification object]; float duration = [moviePlayer duration];
Zunächst verschaffen wir uns das Player-Objekt von der Notifikation (da wir kein Datenobjekt angegeben haben, erhalten wir stattdessen das Objekt, das die Notifikation sendet, also den Movie Player selbst) und können dann über die Eigenschaft duration unmittelbar auf den Wert zugreifen. An dieser Stelle hätten wir auch auf self.player.duration zugreifen können, aber da wir die Methode movieDurationAvailable: auch in ein anderes Objekt stecken könnten, das keinen Zugriff auf den Player hat, nutzen wir das Notifikation-Objekt als entkoppeltere Methode des Zugriffs auf den Player. Nachdem wir mit einfachen Notifikationen umgehen können, sollten wir uns an ein interessanteres Beispiel begeben.
7.4
Eine Wiedergabeliste hinzufügen Da wir nun wissen, wie man Notifikationen vom Player empfängt, können wir diese nutzen, um einen einfachen Playlist-Handler für unseren Player zu schreiben. Beachten Sie allerdings, dass wir hier nicht von iTunes-Wiedergabelisten sprechen; auf die können Sie zugreifen, aber wenn Sie das wollen, müssen Sie einen Blick ins Framework werfen, um zu sehen, wie es funktioniert. Hier werden wir einen Playlist-Controller schreiben, der Wiedergabelisten für unsere eigenen Videomedien verwaltet. Wir werden die Dinge dabei einfach halten und als Modell für unsere Wiedergabeliste ein Array nutzen (in einer echten Anwendung rufen Sie Wiedergabelisten wahrscheinlich aus der Cloud ab). Beginnen wir mit dem PlaylistController: Erstellen Sie dazu in Xcode eine Unterklasse eines Table-View-Controllers namens PlaylistController.m. Öffnen Sie die korrespondierende Header-Datei PlaylistController.h und fügen Sie die folgenden Eigenschaften ein:
W
145
146 X Kapitel 7: Der Movie Player movieplayer/MoviePlayer3withPlaylist/Classes/PlaylistController.h
@interface PlaylistController : UITableViewController { NSArray *items; MoviePlayerViewController *playerController; }
Das Array items soll ein Dictionary für jedes Element der Wiedergabeliste enthalten. Diese enthält den Titel des Elements und den Dateinamen. Die Eigenschaft playerController soll eine Referenz auf unseren MoviePlayerViewController speichern. Richten wir diese Eigenschaft jetzt ein, indem wir eine entsprechende Initialisierungsmethode für die Klasse PlaylistController schreiben: movieplayer/MoviePlayer3withPlaylist/Classes/PlaylistController.m
-(id)initWithPlayer:(MoviePlayerViewController *)thePlayer { self =[super init]; if (self){ playerController = thePlayer; } return self; }
Hier nehmen wir den Player und weisen ihn der lokalen Eigenschaft zu – wir werden den MovePlayerViewController gleich so ändern, dass er diese Klasse instantiiert und den aktiven Movie Player übergibt. Zunächst aber arbeiten wir am Playlist-Controller weiter und erstellen dann mit dem items-Array eine Elementliste für die Wiedergabeliste. Richten Sie das in der Methode viewDidLoad ein: movieplayer/MoviePlayer3withPlaylist/Classes/PlaylistController.m
-(void)viewDidLoad { [super viewDidLoad]; items = [[NSArray arrayWithObjects: [NSDictionary dictionaryWithObjectsAndKeys: @"Introduction",@"title", [playerController movieURL:@"elephantsdream-720-h264-st-aac-1" withFiletype:@"mov"], @"URL", nil], [NSDictionary dictionaryWithObjectsAndKeys: @"Watch out!",@"title", [playerController movieURL:@"elephantsdream-720-h264-st-aac-2" withFiletype:@"mov"], @"URL", nil], [NSDictionary dictionaryWithObjectsAndKeys: @"Follow me!",@"title", [playerController movieURL:@"elephantsdream-720-h264-st-aac-3"
Eine Wiedergabeliste hinzufügen
W
147
withFiletype:@"mov"], @"URL", nil], nil] retain]; }
Hier instantiieren wir ein NSArray mit Elementen des Typs NSDictionary. Jedes Dictionary hat zwei Schlüssel: einen Titel und eine URL für den Film. Als Filme nutzen wir drei Clips, die wir aus dem Film Elephants Dream geschnitten haben. Die Clips finden Sie in Ihrem Projekt, aber Sie können in viewDidLoad auch eigenen Filme und Titel angeben.3 Nachdem wir das Modell erledigt und einen Controller eingerichtet haben (dem wir gleich noch ein paar Dinge hinzufügen werden), müssen wir unserer Anwendung einen View verschaffen. Dazu werden wir der MoviePlayerViewController.m-Implementierung einen Table-View hinzufügen. Schreiben Sie am Ende der Methode viewDidLoad, unmittelbar vor der Zeile, in der player.view als Subview eingefügt wird, folgenden Code: movieplayer/MoviePlayer3withPlaylist/Classes/MoviePlayerViewController.m
PlaylistController *playlist = [[PlaylistController alloc] initWithPlayer:self]; CGRect rect2 = CGRectMake(64, 600, 640, 350); playlist.view.frame = rect2; [self.view addSubview:playlist.view];
Hier instantiieren wir einen PlaylistController (wie Sie sich erinnern werden, eine Unterklasse von UITableViewController), setzen seinen Frame und fügen ihn dann als Subview zum View des MoviePlayerViewController hinzu. Nehmen wir jetzt einige Modifikationen vor, damit der Table-View das für die Anwendung geeignete Aussehen erhält. Fügen Sie dazu der Methode viewDidLoad im PlaylistController folgenden Code hinzu: movieplayer/MoviePlayer3withPlaylist/Classes/PlaylistController.m
UITableView *table = self.tableView; [table setOpaque:NO]; [table setRowHeight:30]; [table setBackgroundColor:[UIColor clearColor]]; [table setSeparatorStyle:UITableViewCellSeparatorStyleNone]; [table setIndicatorStyle:UIScrollViewIndicatorStyleWhite];
Das sind rein ästhetische Änderungen, die uns eine ansehnliche schwarze Tabelle ohne Trennlinien mit weißen Buchstaben und einem Hintergrundverlauf bescheren. Außerdem müssen wir ändern, wie die 3 Beachten Sie, dass dieser Film dem Copyright der Blender Foundation unterliegt und unter http://www. elephantsdream.org zu finden ist.
148 X Kapitel 7: Der Movie Player Zellen angezeigt werden, und haben Ihnen deswegen eine Implementierung von tableView:cellForRowAtIndexPath:indexPath: angeboten, die Sie nutzen können, wenn Sie sich die Sache ansehen wollen (aber eigentlich ist das nicht das, worum es in diesem Kapitel geht). Jetzt benötigen wir ein paar Daten für die Tabelle, damit Sie sehen können, wie Tabelle und Daten tatsächlich aussehen. Dazu werden wir die Datenquellenmethoden in UITableViewController implementieren. Beginnen wir mit den Methoden numberOfSectionsInTableView: und tableView:numberOfRowsInSection:, die recht einfach sind. Für die Anzahl der Abschnitte in der Tabelle werden Sie eins liefern und für die Zeilenanzahl die Zahl der Elemente im Array: movieplayer/MoviePlayer3withPlaylist/Classes/PlaylistController.m
-(NSInteger)numberOfSectionsInTableView: (UITableView *)tableView { return 1; } -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [items count]; }
Jetzt sind Sie für den interessanten Teil bereit: Wird ein Element in der Wiedergabeliste ausgewählt, müssen wir den Film ändern, den der Player abspielt, und stattdessen das neu ausgewählte Element wiedergeben lassen: movieplayer/MoviePlayer3withPlaylist/Classes/PlaylistController.m
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSURL *url = [[items objectAtIndex:indexPath.row] objectForKey:@"URL"]; playerController.player.contentURL = url; [playerController.player play]; }
Wir nutzen dazu hier den indexPath des ausgewählten Elements und ermitteln seine Zeile. Kennen wir die Zeile des Elements, können wir über das items-Array auf die Inhalts-URL des Films zugreifen, die wir der Variablen url zuweisen. Sobald wir die URL haben, können wir die contentURL-Eigenschaft des Players einsetzen (die wir zu Anfang des Kapitels genutzt haben), um dem Player einen anderen Film anzugeben. Und schließlich rufen wir auf dem Player die Methode play auf, um die Wiedergabe des neuen Films anzustoßen. Jetzt können Sie auf „Build and Run“ klicken, um sich die Sache anzusehen!
Eine Wiedergabeliste hinzufügen Bei Ihrem Testlauf haben Sie wahrscheinlich bemerkt, dass das erwartete Verhalten eintritt, wenn Sie auf ein Element in der Wiedergabeliste tippen – der Film wird abgespielt. Aber wenn der Clip abgelaufen ist, wird nicht automatsch der nächste Clip auf der Wiedergabeliste abgespielt. Das werden wir gleich mithilfe einer Notifikation beheben. Über das NSNoticationCenter werden wir unseren Playlist-Controller als Beobachter für die MPMoviePlayerPlaybackDidFinishNotication-Notifikation registrieren. Diese tritt auf, wenn die Wiedergabe eines Films abgeschlossen ist. Hängen Sie dazu ans Ende der Methode viewDidLoad den folgenden Code an: movieplayer/MoviePlayer3withPlaylist/Classes/PlaylistController.m
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerPlaybackDidFinish:) name:MPMoviePlayerPlaybackDidFinishNotification object:nil];
Beachten Sie, dass die damit ausgelöste Notifikation den Selektor playerPlayback-DidFinish: aufruft – schreiben Sie diesen jetzt: movieplayer/MoviePlayer3withPlaylist/Classes/PlaylistController.m
-(void) playerPlaybackDidFinish:(NSNotification*)notification { UITableViewController *tv = self; int rows = [tv.tableView numberOfRowsInSection:0]; int selectedRow = [self.tableView indexPathForSelectedRow].row; if ((selectedRow + 1) < rows) { NSIndexPath *path = [NSIndexPath indexPathForRow:selectedRow+1 inSection:0]; [self.tableView selectRowAtIndexPath:path animated:YES scrollPosition:YES]; playerController.player.contentURL = [[items objectAtIndex:selectedRow+1] objectForKey:@"URL"]; [playerController.player play]; } }
Gehen wir das Schritt für Schritt durch: Der erste Abschnitt des Codes ermittelt die aktuell in der Tabelle ausgewählte Zeile und speichert diese in der Variablen selectedRow. Dann prüfen wir, ob die Tabelle nach der aktuellen Zeile eine weitere Zeile enthält, und erstellen, wenn das der Fall ist, den Pfad zur nächsten Zeile. Wir fordern den TableView über die Methode selectRowAtIndexPath:animated:scrollPosition: auf, diese Zeile zur ausgewählten Zeile zu machen. Beachten Sie,
W
149
150 X Kapitel 7: Der Movie Player dass diese Methode keinen Aufruf unserer tableView:didSelectRowAtIndexPath:-Methode bewirkt und wir deswegen die Inhalts-URL zuweisen und den Player auffordern müssen, den Inhalt abzuspielen. Das war’s. Erstellen Sie den Code und führen Sie ihn aus. Wäre das eine kommerzielle Anwendung, gäbe es noch eine Reihe von Details, die wir implementieren sollten. Außerdem könnten wir weitere Funktionalitäten wie die Anzeige von Werbung, von Vor- und Abspännen und Ähnliches ergänzen. Aber Sie kennen jetzt die Grundlagen der Erstellung eigener Wiedergabelisten. Verfolgen wir diesen Weg noch etwas weiter und schauen wir uns an, was man sonst noch mit Notifikationen und dem Player machen kann.
7.5
Thumbnails erstellen Eine der interessanteren Fähigkeiten des MPMoviePlayerController ist die Möglichkeit, Thumbnails aus den Inhalten der Videos zu erstellen. Der Controller bietet zwei Methoden zur Erstellung von Thumbnails, eine synchrone und eine asynchrone. Der synchrone Aufruf, thumbnail ImageAtTime:timeOption:, generiert jeweils ein Vorschaubild, während die asynchrone Version, requestThumbnailImagesAtTimes:timeOption:, mehrere Vorschaubilder erstellen kann. Wenn Sie einen ganzen Satz von Thumbnails erzeugen wollen, sollten Sie wegen der Auswirkungen auf die Leistung, die viele blockierende synchrone Aufrufe mit sich bringen, unbedingt den asynchronen Aufruf nutzen. Beachten Sie, dass Apple die Generierung von Thumbnails aktuell auf Videoinhalte aus dem lokalen Dateisystem beschränkt und für progressiv heruntergeladene oder gestreamte Inhalte nicht unterstützt. Schauen wir uns kurz die synchrone Methode an, bevor wir uns dem asynchronen Aufruf zuwenden; Sie werden sehen, dass die Parameter für beide Aufrufe recht ähnlich sind: -(UIImage *)thumbnailImageAtTime:(NSTimeInterval)playbackTime timeOption:(MPMovieTimeOption)option
Die Methode thumbnailImageAtTime erwartet eine playbackTime und eine timeOption als Parameter und liefert ein Vorschaubild in Form eines UIImage. Die Wiedergabezeit wird als NSTimeInterval angegeben. Sollte Ihnen dieser Typ noch nicht begegnet sein: Er ist als Double-Wert definiert, dessen Vorkommaanteil die Sekunden und dessen Nachkommaanteil die Sekundenbruchteile darstellt.
Thumbnails erstellen timeOption kann zwei Werte haben: MPMovieTimeOptionNearestKeyFrame oder MPMovieTimeOptionExact. Bei der Option MPMovieTimeOptionExact wird die genaue Zeitposition im Video genutzt, um den Thumbnail zu generieren. Bei der Option MPMovieTimeOptionNearestKey Frame wird stattdessen der nächste Keyframe verwendet, der meh-
rere Sekunden von der angegebenen Zeit abweichen kann. Beachten Sie, dass MPMovieTimeOptionNearestKeyFrame die effizientere Wahl ist. Schauen wir uns jetzt die asynchrone Version an: -(void)requestThumbnailImagesAtTimes:(NSArray *)playbackTimes timeOption:(MPMovieTimeOption)option
Es gibt zwei wesentliche Unterschiede. Zum einen erwartet die asynchrone Version ein Array mit Wiedergabepositionen anstelle einer einzelnen Zeitangabe, und zum anderen hat die Methode den Rückgabetyp void. Aber wenn die Methode den Rückgabetyp void hat, wie gibt sie uns dann unsere Vorschaubilder? Wir nutzen Notifikationen. Wird diese Methode aufgerufen, verarbeitet sie die Vorschaubilder im Hintergrund und sendet die Notifikation MPMoviePlayerThumbnailImageRequestDidFinishNotication, wenn ein Vorschaubild fertig ist. Wir müssen also nur die entsprechende Notifikation überwachen und die Bilder abrufen.
Einen View für die Thumbnails einrichten Vermutlich haben Sie mittlerweile erraten, dass wir für unsere Anwendung Code schreiben werden, der Vorschaubilder generiert und anzeigt. Außerdem werden wir den Vorschaubildern Verhalten geben – werden sie berührt, wird zur entsprechenden Position des Films gesprungen. Zunächst benötigen wir dazu eine geeignete Methode zur Verwaltung der Vorschaubilder in unserer Oberfläche. Wir werden uns also erst mal der Oberfläche widmen und dann den Code schreiben, der die Vorschaubilder für unsere gesamte Videodatei erzeugt. Bevor wir das tun, sollten wir erwähnen, dass wir die gerade geschaffene Informationsanzeige weglassen werden. Aber im nächsten Kapitel wird sie auf ausgefeiltere Weise zurückkehren. Da wir eine stark interaktive Anzeige und eine Interaktion mit unseren Vorschaubildern haben wollen, nutzen wir einen UIScrollView. Wie Sie in der folgenden Abbildung sehen, werden wir die Vorschaubilder horizontal nebeneinander anzeigen und dem Benutzer erlauben, sich durch Wischen in ihnen zu bewegen. Außerdem soll eine einfache Berührung die entsprechende Videoposition in unserem Player erscheinen lassen.
W
151
152 X Kapitel 7: Der Movie Player Auch wenn das kompliziert klingen mag, erfordert es nur sehr wenig Gestencode, den wir im Prinzip aus Kapitel 3, Gesten nutzen, übernommen haben.
Bringen wir zunächst den UIScrollView in die Oberfläche. Öffnen Sie die MoviePlayerViewController.xib-Nib-Datei und ziehen Sie einen Scroll-View in eine Position unmittelbar unterhalb des Players. Unsere Größen- und Attributeinstellungen sehen Sie in Abbildung 7.4
Abbildung 7.4: Ein Scroll-View zur Anzeige von Vorschaubildern
Thumbnails erstellen Speichern Sie Ihre Änderungen im Interface Builder, öffnen Sie die Datei MoviePlayerViewController.h, fügen Sie ein IBOutlet für den Scroll-View hinzu (wie im folgenden Code zu sehen) und kehren Sie dann in den Interface Builder zurück, um das Outlet mit dem neuen Scroll-View zu verbinden: movieplayer/MoviePlayer4/Classes/MoviePlayerViewController.h
@interface MoviePlayerViewController : UIViewController { UIView *viewForMovie; MPMoviePlayerController *player; X UIScrollView *thumbnailScrollView; } @property (nonatomic, retain) IBOutlet UIView *viewForMovie; @property (nonatomic, retain) MPMoviePlayerController *player; X @property (nonatomic, retain) IBOutlet UIScrollView *thumbnailScrollView; -(NSURL *)movieURL; @end
Nachdem Sie das erledigt haben, können wir uns dem eigentlichen Code zuwenden.
Den Thumbnail-Code schreiben Nehmen wir an, dass wir 20 Vorschaubilder in gleichmäßigem Abstand für unser Video erstellen wollen (oder ein beliebiges anderes, das wir mit unserer Anwendung betrachten wollen). Wir müssen die Dauer des Videos kennen und erhalten sie, wie wir bereits erfahren haben, erst, nachdem wir die Notifikation MPMovieDurationAvailableNotication bekommen haben. Glücklicherweise haben wir im letzten Abschnitt eine Methode mit dem Namen movieDurationAvailable: geschrieben, die aufgerufen wird, wenn die Dauer verfügbar ist. Löschen wir den Code in movieDurationAvailable: und fügen wir in die Methode stattdessen den folgenden Code ein: movieplayer/MoviePlayer4/Classes/MoviePlayerViewController.m
-(void) movieDurationAvailable:(NSNotification*)notification { float duration = [self.player duration]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerThumbnailImageRequestDidFinish:) name:MPMoviePlayerThumbnailImageRequestDidFinishNotification object:nil];
W
153
154 X Kapitel 7: Der Movie Player NSMutableArray *times = [[NSMutableArray alloc] init]; for(int i = 0;i < 20; i++){ float playbackTime = i * duration/20; [times addObject:[NSNumber numberWithInt:playbackTime]]; } [self.player requestThumbnailImagesAtTimes:times timeOption: MPMovieTimeOptionExact]; }
Zunächst rufen wir die Dauer des Videos direkt vom Video-Player ab.4 Bevor wir damit beginnen, die Anfragen nach Thumbnails zu starten, registrieren wir uns für die Notifikation MPMoviePlayerThumbnailImageRequestDidFinishNotication und leiten derartige Notifikationen an die Methode playerThumbnailImageRequestDidFinish:, die wir gleich schreiben werden. Jetzt können wir uns daran machen, die Thumbnail-Anfragen einzurichten. Erinnern Sie sich, dass die Methode requestThumbnailImagesAtTimes:timeOption: ein Array mit den Zeiten erwartet, für die wir Vorschaubilder haben wollen. Weil wir 20 Vorschaubilder erzeugen möchten, erstellen wir ein NSMutable-Array und fügen ihm in einer Schleife mit 20 Durchläufen eine Zeit hinzu, die auf Basis der durch 20 geteilten Spieldauer berechnet wird. Dann rufen wir die Methode requestThumbnailImagesAtTimes:timeOption: mit dem Array auf und nutzen die Option MPMovieTimeOptionExact, um anzuzeigen, dass die Vorschaubilder den übergebenen Zeitpunkten so nah wie möglich sein sollen (und opfern daher die Effizienz der Genauigkeit). Wir haben also gewartet, bis die Metadaten des Players verfügbar sind, haben uns auf Basis dieser Metadaten für Thumbnail-Notifikationen registriert und eine asynchrone Anfrage abgesetzt, die den Player auffordert, 20 Vorschaubilder zu generieren. Jetzt müssen wir die Methode playerThumbnailImageRequestDidFinish: schreiben, um die Vorschaubilder zu verarbeiten, wenn sie generiert werden.
Die Thumbnails verarbeiten Die Thumbnail-Generierung führt zu einer Notifikation, deren userInfo-Dictionary zwei für uns interessante Elemente enthält: den Wert für den Schlüssel MPMoviePlayerThumbnailTimeKey als NSNumber Objekt mit der Zeitposition des Vorschaubilds und den Wert zum Schlüssel MPMoviePlayerThumbnailImageKey mit dem Vorschaubild selbst. Im Code rufen wir beide Objekte ab und nutzen dann die Hilfs4 Wie Sie sich erinnern werden, hätten wir das Player-Objekt auch aus der Notifikation abrufen können, wie wir es im Code auf Seite 144 gemacht haben.
Thumbnails erstellen
W
155
methode makeThumbnailImageViewFromImage:andTimeCode:, um einen Image-View zu erstellen, den wir als Subview unserem Scroll-View hinzufügen. movieplayer/MoviePlayer4/Classes/MoviePlayerViewController.m
-(void) playerThumbnailImageRequestDidFinish:(NSNotification*)notification { NSDictionary *userInfo = [notification userInfo]; NSNumber *timecode = [userInfo objectForKey: MPMoviePlayerThumbnailTimeKey]; UIImage *image = [userInfo objectForKey: MPMoviePlayerThumbnailImageKey]; ImageViewWithTime *imageView = [self makeThumbnailImageViewFromImage:image andTimeCode:timecode]; [thumbnailScrollView addSubview:imageView]; }
Sie sehen, dass die makeThumbnailImageViewFromImage:andTimeCode:Methode ein ImageViewWithTime-Objekt statt des üblichen UIImageView liefert. Diese Klassen unterscheiden sich nur darin, dass ImageViewWithTime eine zusätzliche Eigenschaft hat, die genutzt wird, um eine Zeitposition unmittelbar mit einem Bild zu verknüpfen. Das werden wir nutzen, wenn der Benutzer das Vorschaubild berührt. Die Methode makeThumbnailImageViewFromImage:andTimeCode: kümmert sich auch um die Einrichtung des Frames des ImageViewWithTime-Objekts, damit es an der korrekten Position des Scroll-View eingefügt wird. Sollten Sie die Einzelheiten interessieren, finden Sie die entsprechende Methode in der MoviePlayerViewController.m-Implementierung. Unterbrechen Sie das Programmieren hier für einen Augenblick, kompilieren Sie die Anwendung und lassen Sie ein paar Vorschaubilder generieren. Haben Sie die Anwendung ausgeführt, sollten 20 Vorschaubilder erstellt und dem Scroll-View hinzugefügt worden sein. Wenn Sie nun mit dem Finger über den View wischen, müssten Sie alle Bilder anschauen können. Sie können jedoch noch nicht mit einer Berührung des Vorschaubilds zur entsprechenden Position des Videos gehen. Aber nach all den Vorarbeiten sollte es jetzt kein großes Problem mehr sein, diesen Code zu schreiben. Genauer gesagt, werden wir im Prinzip einfach den Gestencode übernehmen, den wir in Kapitel 3, Gesten nutzen, zur Verarbeitung von Touch-Events genutzt hatten. Und zwar so:
156 X Kapitel 7: Der Movie Player movieplayer/MoviePlayer4/Classes/MoviePlayerViewController.m
-(void) playerThumbnailImageRequestDidFinish:(NSNotification*)notification { NSDictionary *userInfo = [notification userInfo]; NSNumber *timecode = [userInfo objectForKey: MPMoviePlayerThumbnailTimeKey]; UIImage *image = [userInfo objectForKey: MPMoviePlayerThumbnailImageKey]; ImageViewWithTime *imageView = [self makeThumbnailImageViewFromImage:image andTimeCode:timecode]; [thumbnailScrollView addSubview:imageView]; UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapFrom:)]; [tapRecognizer setNumberOfTapsRequired:1];
X X X X X X X
[imageView addGestureRecognizer:tapRecognizer]; [tapRecognizer release]; }
Hier erstellen wir einen Recognizer für eine Einfach-Tap-Geste und geben ihm das Ziel self sowie die Aktion handleTapFrom:. Dann binden wir diesen Recognizer an die Vorschaubilder, damit diese, wenn auf sie getippt wird, die Methode handleTapFrom: aufrufen. Jetzt müssen wir nur noch die Aktion für die Geste implementieren: movieplayer/MoviePlayer4/Classes/MoviePlayerViewController.m
-(void)handleTapFrom:(UITapGestureRecognizer *)recognizer { ImageViewWithTime *imageView = recognizer.view; self.player.currentPlaybackTime = [imageView.time floatValue]; }
Die Methode handleTapFrom: beschafft sich zunächst den View, auf dem die Geste erkannt wurde (einer der Image-Views im Scroll-View) und setzt dann die currentPlaybackTime des Players auf die Zeit, die im ImageViewWithTime gespeichert ist. Das sorgt dafür, dass der Videoinhalt des Players ab der Position abgespielt wird, die diese Zeit angibt. Jetzt können Sie den Code erneut erstellen und ausführen. Tippen Sie auf eins der Vorschaubilder, werden Sie unmittelbar zur entsprechenden Position des Videoinhalts geführt (unseren Test sehen Sie in Abbildung 7.5).
Vorschau W 157
Abbildung 7.5: Der Thumbnail-View mit dem Movie Player
7.6
Vorschau In diesem Kapitel haben Sie sich mit folgenden Grundlagen vertraut gemacht: MPMoviePlayerController instantiieren, einen View für den anzuzeigenden Film erstellen, Eigenschaften der Wiedergabe untersuchen, Notifikationen zum Zustand des Players überwachen, Vorschaubilder für Videos erstellen. Wir haben sogar eine einfache Wiedergabeliste geschaffen. Im nächsten Kapitel werden wir einen Schritt weitergehen und uns ansehen, wie man Views erstellt, die den Videoinhalt überlagern, wie man eine eigene Wiedergabesteuerung anbietet, und sogar einen Blick darauf werfen, wie man Videos im Vollbildmodus anzeigt. Und in dem daran anschließenden Kapitel werden wir uns gar dem Streaming von Videoinhalten zuwenden.
Kapitel 8
Der Movie Player, Phase 2 Nachdem Sie ein vollständiges Kapitel mit dem MPMoviePlayerController verbracht haben, wissen Sie eine Menge darüber, wie man den Player steuert sowie von ihm Statusinformationen erhält und Notifikationen empfängt, wenn sich sein Zustand ändert. Sie wissen auch, wie man Vorschaubilder mit Videoinhalten für Filme erstellt, die lokal auf dem Gerät gespeichert sind. In diesem Kapitel werden wir einen Schritt weitergehen und dem Player zusätzliche Interaktivität verleihen, indem wir zunächst zeitbasierte Nachrichten anzeigen, die das Video überlagern, und dann eigene Steuerelemente zum Abspielen erstellen. Schließlich werden wir einige letzte Feinheiten ergänzen, indem wir uns dem Umgang mit dem Vollbildmodus zuwenden.
8.1
Video-Shoutouts Der View des MPMoviePlayerController kann nicht nur zur Anzeige des Videoinhalts genutzt werden. Obwohl Sie mit diesem View umsichtig umgehen und keine seiner Eigenschaften direkt ändern sollten, können Sie ihm eigene Subviews hinzufügen, und das bietet uns eine Möglichkeit, das Video durch anderen Inhalt zu überlagern. In diesem Abschnitt werden wir diese Fähigkeit nutzen, um ein einfaches Shoutout-System einzurichen, über das wir zu bestimmten Zeiten während der Wiedergabe des Videos Meldungen anzeigen lassen können – wenn Sie „Popup-Videos“ im VH-1-Stil kennen, wissen Sie, was wir da im Kopf haben (in Abbildung 8.1 auf der folgenden Seite finden Sie eine Vorschau). Zu diesem Zweck haben wir in das MoviePlayer5Projekt eine CommentView-Klasse eingefügt, die eine sprechblasenartige
160 X Kapitel 8: Der Movie Player, Phase 2 Grafik samt Alpha-Schatten und darüber angezeigtem Text bietet (werfen Sie ruhig einen Blick in den Code, wenn Sie die Sache interessiert, aber hier würde das doch etwas vom Thema wegführen).
Abbildung 8.1: Der Thumbnail-View mit dem Movie Player Bevor wir mit dem Programmieren beginnen, sollten Sie sich noch mal vergegenwärtigen, dass wir hier nur zeigen wollen, wie man Subviews in Videos einfügt. Wir wollen kein ausgefeiltes System zur Steuerung von Textmeldungen implementieren. Man könnte das Modell und die Logik, die wir für die Shoutouts nutzen, gar als peinlich primitiv betrachten. Doch so einfach es auch sein mag, lassen Sie uns erst mal einen Blick darauf werfen: movieplayer/MoviePlayer5/Classes/MoviePlayerViewController.m
-(void)viewDidLoad { [super viewDidLoad]; shoutOutTexts = [[NSArray arrayWithObjects: @"This film\nwas rendered using\ncloud computing ", @"Look out\nFrank, Rinky\nand Gamera!", nil] retain];
Video-Shoutouts shoutOutTimes = [[NSArray arrayWithObjects: [[NSNumber alloc] initWithInt: 2], [[NSNumber alloc] initWithInt: 325], nil] retain]; position = 0; // Der Rest von viewDidLoad }
Wir speichern die Shoutouts in zwei einfachen Arrays: einem für die Zeitposition und einem, das den Text der Shoutouts aufnimmt. Beispielsweise könnten wir “Hallo Mama!” und 17 speichern, um anzuzeigen, dass der Text “Hallo Mama!” nach 17 Sekunden der Wiedergabe angezeigt werden soll. Beachten Sie, dass wir voraussetzen, dass die Zeiten in aufsteigender Reihenfolge angegeben werden. Zusätzlich haben wir die Integer-Instanzvariable position eingefügt, die wir hier auf null setzen (zu ihr werden wir gleich zurückkehren), um den aktuellen Shoutout festzuhalten. Damit wir wissen, wann die Shoutouts angezeigt werden sollen, müssen wir die Wiedergabe im Blick behalten. Dazu werden wir die Klasse NSTimer nutzen, um in regelmäßigen Abständen eine Methode aufzurufen. Fügen Sie folgenden Code der Methode viewDidLoad unmittelbar unter der Instantiierung der Shoutout-Arrays hinzu: movieplayer/MoviePlayer5/Classes/MoviePlayerViewController.m
[NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(checkShoutouts:) userInfo:nil repeats:YES];
Hier nutzen wir den NSTimer, um in einer Sekunde einen Methodenaufruf auszuführen, dessen Ziel die checkShoutouts-Methode in dieser Klasse ist. Außerdem soll der Aufruf der Methode sekündlich wiederholt werden. Jetzt müssen wir die checkShoutouts-Methode schreiben, die vom Timer aufgerufen wird. Diese wird so funktionieren: Wir werden eine Instanzvariable namens position nutzen, die als Zeiger auf den Shoutout dient, der als Nächstes angezeigt werden soll. Jedes Mal, wenn die Methode checkShoutouts vom Timer aufgerufen wird, werden wir die currentPlaybackTime des Players mit der Zeit des aktuellen Shoutouts vergleichen, den Shoutout anzeigen und position erhöhen, wenn currentPlaybackTime kleiner oder gleich der aktuellen Shoutout-Zeit ist.
W
161
162 X Kapitel 8: Der Movie Player, Phase 2 Die Vergleichslogik haben wir in eine eigene Methode namens isTimeForNextShoutout ausgelagert: movieplayer/MoviePlayer5/Classes/MoviePlayerViewController.m
-(BOOL)isTimeForNextShoutout { int count = [shoutOutTimes count]; if (position < count) { int timecode = [[shoutOutTimes objectAtIndex:position] intValue]; if (self.player.currentPlaybackTime >= timecode) { return YES; } } return NO; }
Nutzen wir jetzt diese Methode, um checkShoutouts: zu schreiben. Zunächst müssen wir prüfen, ob es ein neues Shoutout anzuzeigen gibt. Ist das der Fall, instantiieren wir einen CommentView mit dem Text des Shoutout und fügen diesen dann dem View des Players als Subview hinzu. Überlegen Sie, was hier geschieht: Wir haben einfach einen View genommen, diesen aber nicht dem primären View der Anwendung, sondern dem View hinzugefügt, den der Player zur Wiedergabe des Videos nutzt – der auch als MPMoviePlayerController bekannt ist. Das bewirkt, dass der View unmittelbar über dem Videoinhalt angezeigt wird. Das werden wir gleich in Aktion sehen. movieplayer/MoviePlayer5/Classes/MoviePlayerViewController.m
-(void)checkShoutouts:(NSTimer*)theTimer { if ([self isTimeForNextShoutout]) { CommentView *commentView = [[CommentView alloc] initWithText:[shoutOutTexts objectAtIndex:position++]]; [self.player.view addSubview:commentView]; [NSTimer scheduledTimerWithTimeInterval:4.0f target:self selector:@selector(removeView:) userInfo:commentView repeats:NO]; } }
Jetzt steht nur noch eine Aufgabe aus – irgendwann müssen wir das Shoutout entfernen. Wir werden einen weiteren NSTimer erstellen, der nach vier Sekunden sein Ziel anstößt. Außerdem werden wir den userInfo-Parameter nutzen, der der Aktionsmethode mit unserem Shoutout-View übergeben wird.
Eigene Wiedergabesteuerung W 163 Als Aktion werden wir nach vier Sekunden die Methode removeView aufrufen. Das, was in removeView geschieht, ist sehr einfach: Wir beschaffen uns aus dem userInfo-Parameter eine Referenz auf den Shoutout-View und entfernen diesen aus seinem Superview (präziser, aus dem Video-View des MPMoviePlayerController). movieplayer/MoviePlayer5/Classes/MoviePlayerViewController.m
-(void)removeView:(NSTimer*)theTimer { UIView *view = [theTimer userInfo]; [view removeFromSuperview]; }
Damit ist unser Shoutout-Code abgeschlossen. Kompilieren Sie ihn, führen Sie den Code aus und erfreuen Sie sich an dem Anblick. Beachten Sie die Alpha-Schatten unmittelbar über dem Video. Sie können sich eigene Shoutouts ausdenken oder, besser noch, ein eigenes Shoutout-System implementieren.
8.2
Eigene Wiedergabesteuerung Jetzt haben wir uns schon eine Menge Wissen einverleibt: Wir wissen, wie man Statusinformationen abruft, Notifikationen empfängt und der Videoanzeige eigene Subviews hinzufügt. Nun werden Sie dieses Wissen einsetzen, um eine eigene Wiedergabesteuerung für den Player aufzubauen. Warum man wohl auf so eine Idee kommen könnte? Na, dann werfen Sie einmal einen Blick auf Apples eingebaute Steuerelemente:
Apple bietet die übliche Implementierung für die Wiedergabesteuerung, einschließlich eines Play-/Pause-Buttons, eines Schiebers, um die Position im Video zu wählen, einer Zeitanzeige und einem Button für den Vollbildmodus. Nicht schlecht, aber will man nicht manchmal seinen Benutzern weitere Möglichkeiten anbieten (Optionen für soziale Netzwerke beispielsweise), das Aussehen der Steuerelemente anpassen oder es passend zu einem bestimmten Thema im Inhalt gestalten? Wir finden das sinnvoll und wollen uns deswegen jetzt ansehen, wie man eine Wiedergabesteuerung entwirft, die in Form eines HUD-Displays unmittelbar über dem Videoinhalt erscheint. So stellen wir uns die Sache vor:
164 X Kapitel 8: Der Movie Player, Phase 2
Sie müssen doch zugeben, dass das eine Verbesserung ist, oder? Gut, wir haben die Gestaltung und Anzeige der Steuerelemente nicht auf die Spitze getrieben – das überlassen wir Ihnen und Ihren Anwendungen –, aber wir haben ansprechendere Bedienelemente geschaffen, die sich unmittelbar in der Mitte des Videos befinden. Wir bieten die grundlegenden Elemente, die auch Apple bietet, und haben sogar einige neue Funktionen angedeutet, die man hinzufügen könnte (beispielsweise die Sprechblasen, die eine Sharing-Fähigkeit repräsentieren, die wir zwar nicht implementieren werden, die aber auf Sie wartet). Beachten Sie, dass wir auch eine eigene Version des Schiebers eingefügt und nicht Apples Standarddarstellung verwendet haben. Die Herstellung grafischer Repräsentationen für eine neue Wiedergabesteuerung ist recht einfach, aber die Implementierung einer Wiedergabesteuerung, die so gut funktioniert wie die von Apple, verlangt sorgfältige Arbeit. Überlegen wir, was eine Wiedergabesteuerung tun muss, damit sie ordentlich funktioniert. Wir müssen den Status des Players kennen. Spielt er? Wurde er angehalten? Sucht der Benutzer innerhalb des Videos? An welcher Wiedergabeposition befindet er sich? Haben wir die Zeitposition und die Position des Wiedergabereglers angepasst? Versucht der Benutzer den Status des Players zu ändern (beispielsweise von Abspielen zu Pause)? Möchte der Benutzer in den Vollbildmodus wechseln? Was tun wir, wenn die Wiedergabe das Ende des Videos erreicht? Offensichtlich gibt es eine Menge Einzelheiten, die unsere Implementierung ziemlich komplex machen könnten, aber mithilfe all der Mittel, die wir bereits erlernt haben, können wir unseren Entwurf recht klein und einfach halten.
Eigene Wiedergabesteuerung W 165 Auf den folgenden Seiten werden wir nach und nach in jede dieser Einzelheiten eintauchen und erst wieder ans Licht kommen, wenn alles funktioniert.
Den View erstellen Wir werden damit beginnen, dass wir die Wiedergabesteuerung als eigenständigen View und View-Controller aufbauen und diese dann in den View des Movie Player einbauen. Öffnen Sie zunächst das Projekt movieplayer/MoviePlayer6. Sie werden feststellen, dass wir seit dem letzten Projekt einige neue Elemente hinzugefügt haben: einige neue Bilder in der Gruppe Images und außerdem eine neue Nib-Datei namens PlaybackViewController.xib. Klicken Sie doppelt auf die Nib, um sie im Interface Builder zu öffnen, sehen Sie die folgende Schnittstelle:
Wenn Sie sich den Entwurf für die Wiedergabesteuerung ansehen, die wir gerade vorgestellt haben, sehen Sie, dass die Dinge etwas anders aussehen: Zunächst sieht der Schieberegler wie der übliche Apple-Regler aus, nicht wie unser eigener – das ist so in Ordnung. Das gewünschte Aussehen werden wir gleich mit Code umsetzen. Außerdem sieht die Oberfläche einfarbig grau aus, obwohl wir eigentlich doch ansehnliche transparente Bedienelemente wollten. Aber auch das ist okay. Die Nib-Schnittstelle liegt vor einem einfarbig weißen Hintergrund, der bewirkt, dass man die Transparenz nur schwer erkennen kann. Haben wir die Schnittstelle erst über das Video gelagert, werden Sie sehen, dass der Hintergrund semitransparent ist (mehr Informationen zu semitransparenten Views finden Sie im Kasten „Die semitransparente Oberfläche erstellen“ auf Seite 167). Nehmen Sie sich einige Minuten Zeit, um sich diese Nib genau anzusehen. Betrachten Sie die Views, die wir hinzugefügt haben, sowie die damit verbundenen Texte und Bilder, die wir verwendet haben. Beachten Sie beispielsweise, dass der Play-/Pause-Button zwei Zustände und jeweils ein Bild für jeden dieser Zustände hat.
166 X Kapitel 8: Der Movie Player, Phase 2 Wenn Sie sich genau ansehen wollen, was wir gemacht haben, könnte die Listenansicht im Interface Builder nützlich sein. Sie sollten Folgendes sehen:
Ein kleiner Rundflug: Sie sollten das Hintergrundbild sehen, das als visueller Container für alle Steuerelemente dient. Dann haben wir mehrere UILabelViews, von denen einige als einfache Textlabels dienen, während andere genutzt werden, um Echtzeitinformationen wie die Zeit und den Status anzuzeigen. Außerdem sehen Sie einen UISlider. Das ist unser Steuerelement zur Navigation im Video (das man auch als Scroll- oder Fortschrittsleiste bezeichnet). Außerdem finden Sie einige Buttons: den von uns bereits erwähnten Play-/Pause-Button, der zwischen Abspielen und Pause umschaltet, den Vollbild-Button, der in die Vollbildwiedergabe von Videos wechselt, und einen Share-Button (in Form von zwei Sprechblasen), dessen Implementierung wir Ihnen überlassen (wir wollen hier diese Funktionalität nicht implementieren, sondern zeigen, dass man die elementaren Steuerelemente erweitern kann, um neue Funktionen anzubieten). Damit haben wir die Designarbeit im Interface Builder hinter uns, und es folgt die wahre Arbeit – dem Ganzen Leben einzuhauchen. Erstellen wir dazu einige IBActions und IBOutlets und verbinden wir dann einen Controller mit diesem View.
Eigene Wiedergabesteuerung W 167
Den Controller erstellen Gehen Sie zu Xcode zurück und erstellen Sie einen View-Controller für die Nib: Wählen Sie File → New File... und erstellen Sie eine UIViewController -Subklasse.
Die semitransparente Oberfläche erstellen Im Allgemeinen können Sie jedes Schnittstellenelement semitransparent machen, indem Sie seinen alpha-Wert auf einen Wert kleiner 1.0 setzen. Der Wert 0 entspricht vollständiger Transparenz oder Unsichtbarkeit. So erstellen wir die semitransparente Hintergrundhülle für unsere Schnittstelle: 1. Deaktivieren Sie die opaque-Einstellung auf dem View des PlaybackViewController. 2. Wählen Sie die Hintergrundfarbe des View und setzen Sie ihren Alpha-Wert auf 0. Diese beiden Schritte sichern, dass wir durch den gesamten View hindurchblicken können, den wir erstellen; andernfalls würde der View des PlaybackViewController als gleichförmig weißer View erscheinen. 3. Erstellen Sie den Hintergrund Ihrer Schnittstelle in einem Programm wie Photoshop, setzen Sie seinen Alpha-Wert auf semitransparent (bei uns ungefähr 0,65) und speichern Sie das Bild dann im PNG-Format. 4. Fügen Sie das Bild Ihrem Hauptprojekt hinzu und erstellen Sie im Interface Builder ein UIImageView-Objekt, das das Bild aufnimmt. Beachten Sie, dass Sie einen vergleichbaren Hintergrund auch mit Code erstellen können. Für ein Bild sollten Sie sich entscheiden, wenn Ihre Schnittstelle recht fotografisch oder in einer Weise gestaltet ist, die sich algorithmisch nicht so leicht abbilden lässt.
Sie sollten sicherstellen, dass „With XIB for user interface“ und „UITableViewController subclass“ nicht ausgewählt sind. Nennen Sie den Controller PlaybackViewController.m und klicken Sie auf „Finish“. Öffnen Sie dann PlaybackViewController.h und fügen Sie die folgenden IBOutlets ein (einen für jeden View, auf den wir im Code unseres Controllers zugreifen müssen):
168 X Kapitel 8: Der Movie Player, Phase 2 movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.h
@interface PlaybackViewController : UIViewController { IBOutlet UISlider *playbackSlider; IBOutlet UIButton *playPauseButton; IBOutlet UIButton *fullscreenButton; IBOutlet UILabel *statusLabel; IBOutlet UILabel *timeLabel; }
Kehren Sie in den Interface Builder zurück und verbinden Sie die einzelnen Outlets mit den entsprechenden Views in der Nib-Datei. Außerdem sollten Sie auf File’s Owner klicken und die Klasse auf der Identity-Seite auf PlaybackViewController setzen. Lassen Sie uns zur kurzfristigen Befriedigung unserer Ungeduld den View mit dem Video-Player verbinden, damit wir den Code ausführen und testen können. Anschließend kehren wir zurück und beginnen mit der Implementierung der Verhalten. Öffnen Sie MoviePlayerViewController.m und fügen Sie am Ende der Methode viewDidLoad folgenden Code ein: movieplayer/MoviePlayer6Start/Classes/MoviePlayerViewController.m
self.player.controlStyle = MPMovieControlStyleNone; PlaybackViewController *controls = [[PlaybackViewController alloc] init]; CGRect rect = controls.view.frame; rect.origin.y = 170; rect.origin.x = 90; controls.view.frame = rect; [self.player.view addSubview:controls.view];
Gehen wir das schrittweise durch. Erst setzen wir die controlStyleEigenschaft des Movie Player, die den Stil der eingebauten Bedienelemente steuert. Wir setzen diese Eigenschaft auf MPMovieControlStyleNone und entfernen damit die eingebauten Bedienelemente. Würden wir das unterlassen, erhielte der Benutzer zwei Sätze mit Bedienelementen (unsere und Apples). Anschließend instantiieren wir einen PlaybackViewController, passen seinen Frame so an, dass er ordentlich in den Movie-View passt, und fügen ihn dem View des Players als Subview hinzu. Kompilieren Sie den Code und führen Sie ihn aus. Sie sollten einen funktionierenden (aber noch nicht funktionalen) Satz von Bedienelementen über dem Videoinhalt sehen. Zurzeit werden wir die Bedienelemente permanent über dem Videoinhalt angezeigt lassen. Später werden wir uns darum kümmern, dass sie nur erscheinen, wenn der
Die Steuerelemente implementieren Benutzer den Bildschirm innerhalb des Videos berührt (und verschwinden, wenn er nicht mehr mit ihnen interagiert). Nachdem wir die Wiedergabesteuerung sichtbar in Position haben, können wir mit der Implementierung beginnen.
8.3
Die Steuerelemente implementieren Wenden wir uns jetzt der Aufgabe zu, den Play-/Pause-Button funktionsfähig zu machen. Dazu müssen wir zunächst unseren Bedienelementen eine Referenz auf den Player geben – fügen Sie also der HeaderDatei PlaybackViewController.h eine neue Eigenschaft hinzu: movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.h
MPMoviePlayerController *player;
Ergänzen Sie dann eine neue Initialisierungsmethode namens initWithPlayer: in der PlaybackViewController.m-Implementierungsdatei. movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.m
-(id)initWithPlayer:(MPMoviePlayerController *)thePlayer { self =[super init]; if (nil != self){ player = thePlayer; } return self; }
Neben dem Aufruf der init-Methode der Superklasse weisen wir hier nur den Player einer Eigenschaft in PlaybackViewController zu, die wir bald einsetzen werden. Jetzt müssen Sie die viewDidLoad:-Methode in MoviePlayerController.m aktualisieren, indem Sie ihren init-Aufruf durch einen initWithPlayer:-Aufruf ersetzen und self.player übergeben: movieplayer/MoviePlayer6Start/Classes/MoviePlayerViewController.m
PlaybackViewController *controls = [[PlaybackViewController alloc] initWithPlayer:self.player];
Jetzt sind wir soweit, den eigentlichen Code für den Play-/Pause-Button zu schreiben. Da dieser ein UIButton ist, benötigen wir eine Aktion, die aufgerufen wird, wenn der Button berührt wird. Fügen Sie der Implementierungsdatei PlaybackViewController.m eine Aktion namens handlePlayPause: hinzu:
W
169
170 X Kapitel 8: Der Movie Player, Phase 2 movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.m
-(IBAction)handlePlayAndPauseButton:(id)sender { UIButton *button = (UIButton *)sender; if (button.selected) { button.selected = NO; [player play]; } else { button.selected = YES; [player pause]; } }
Als wir uns die Schnittstelle in Interface Builder angesehen haben, bemerkten wir, dass der Button zwei Zustände hat, einen Play-Zustand (der ungewählte Zustand) und einen Pause-Zustand (der ausgewählte Zustand). Befindet sich der Button im angewählten Zustand, ist der Player angehalten, wir ändern den Zustand also in ungewählt und fordern den Player auf, das Video abzuspielen. Befindet sich der Button hingegen im ungewählten Zustand, setzen wir den Zustand auf ausgewählt und fordern den Player auf, das Video anzuhalten. Jetzt müssen Sie den Button mit dieser Aktion zusammenbringen – verbinden Sie also im Interface Builder den Play-UIButton mit der handlePlayPause:-Methode, indem Sie auf File’s Owner ziehen. Das war’s; klicken Sie auf „Build and Run“ und testen Sie den Play-/Pause-Button.
8.4
Die Wiedergabezeit verwalten Einige Aspekte der Schnittstelle drehen sich um Zeit: das Zeitlabel, in dem die aktuelle Zeit und die absolute Laufzeit des Videos angezeigt werden sollen, und der Schieberegler, der aktualisiert wird, während die Wiedergabe fortschreitet (und dem Benutzer ermöglicht, im Video vor- und zurückzuspulen). Schreiben wir zunächst den Code zur Aktualisierung des Zeitlabels, der große Ähnlichkeiten mit dem Code hat, den Sie im letzten Kapitel geschrieben haben. Vielleicht erinnern Sie sich, dass wir eine MPMovieDurationAvailableNotication-Notifikation einrichten und mit der Verfolgung der Abspielzeit beginnen können, wenn der Player die Dauer des Videos kennt (das werden wir gleich auch nutzen, um den Schieberegler zu aktualisieren). Öffnen Sie dazu PlaybackViewController.m und fügen Sie in die Methode initWithPlayer: folgenden Code ein:
Die Wiedergabezeit verwalten movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.m
X X X X X
-(id)initWithPlayer:(MPMoviePlayerController *)thePlayer { self =[super init]; if (nil != self){ player = thePlayer; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(movieDurationAvailable:) name:MPMovieDurationAvailableNotification object:nil]; } return self; }
Hier haben wir die Notifikation so eingerichtet, dass movieDurationAvailable: aufgerufen wird, wenn der Player die Dauer des Videos kennt. Machen wir uns nun an die Methode movieDurationAvailable:: movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.m
-(void) movieDurationAvailable:(NSNotification*)notification { if (playbackTimer == nil) { playbackTimer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(updatePlaybackTime:) userInfo:nil repeats:YES]; } }
Das sollte Ihnen bekannt vorkommen. Wir werden einen Timer einrichten, der die Wiedergabezeit nachhält, indem er sekündlich die Methode updatePlaybackTime: aufruft. Fügen Sie diese Methode der ControllerDatei hinzu und schauen Sie sich dann die Methode updatePlaybackTime: an: movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.m
-(void)updatePlaybackTime:(NSTimer*)theTimer { float playbackTime = player.currentPlaybackTime; float duration = player.duration; timeLabel.text = [NSString stringWithFormat: @"%.f von %.f Sekunden", playbackTime, duration]; }
Auch das müsste vertraut wirken: Wir rufen die Dauer und die aktuelle Wiedergabeposition vom Player ab, formatieren sie und setzen den Text des timeLabel-Outlets.
W
171
172 X Kapitel 8: Der Movie Player, Phase 2 Da wir gerade dabei sind, können wir der Schnittstelle weitere Statusinformationen hinzufügen, indem wir die MPMoviePlayerPlaybackStateDidChangeNotication-Notifikation nutzen, mit der wir ebenfalls vertraut sind. Richten Sie die Notifikation folgendermaßen in der Methode initWithPlayer: ein: movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.m
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerPlaybackStateDidChange:) name:MPMoviePlayerPlaybackStateDidChangeNotification object:nil];
Definieren Sie jetzt die Methode playerPlaybackStateDidChange: movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.m
-(void) playerPlaybackStateDidChange:(NSNotification*)notification { if ([player playbackState] == MPMoviePlaybackStatePaused) { statusLabel.text = @"Pausiert..."; playPauseButton.selected = YES; } else if ([player playbackState] == MPMoviePlaybackStatePlaying) { statusLabel.text = @"Spielt"; playPauseButton.selected = NO; } else if ([player playbackState] == MPMoviePlaybackStateStopped) { statusLabel.text = @"Angehalten"; playPauseButton.selected = NO; } }
Der eigentliche Zweck dieses Codes ist es, das Statuslabel mit dem aktuellen Zustand des Players (spielend, pausierend, angehalten) zu aktualisieren. Dazu prüfen wir einfach den Status des Players und füllen das Label mit dem entsprechenden Text. Beachten Sie, dass wir bei den Zuständen „spielend“ und „pausierend“ auch den Zustand des Play-/Pause-Buttons setzen. Warum? Erinnern Sie sich, dass eine Statusänderung eintreten könnte, wenn ein anderer Teil unserer Anwendung, der Zugriff auf den Player hat, seinen Status ändert. Wenn das passiert, haben unsere Buttons keine Ahnung, dass sich der Zustand geändert hat. Also überwachen wir diese Notifikation und aktualisieren die Buttons selbst. Wie Sie im folgenden Abschnitt sehen werden, hält der Schieberegler das Video an, wenn der Benutzer im Video spult. Ohne diese Notifikation würde der Play-/Pause-Button fälschlich anzeigen, dass das Video abgespielt wird, während gespult wird.
Eine Videonavigation implementieren W 173
8.5
Eine Videonavigation implementieren Langsam nähern wir uns unserem Ziel. Jetzt müssen wir nur noch den Schieberegler und den Button für den Vollbildmodus implementieren. Die Implementierung des Schiebereglers ist wie erwartet am aufwendigsten, weil wir seinen Fortschritt nachhalten und alles entsprechend aktualisieren müssen. Aber unsere erste Aufgabe besteht darin, ihm das Aussehen zu verleihen, das wir gerne hätten. Beginnen wir dort. Fügen Sie der viewDidLoad:-Methode im Playback-Controller den folgenden Code hinzu: movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.m
[playbackSlider setThumbImage:[UIImage imageNamed:@"thumb.png"] forState:UIControlStateNormal]; UIImage *stretchLeftTrack = [[UIImage imageNamed:@"leftslider.png"] stretchableImageWithLeftCapWidth:5.0 topCapHeight:0.0]; UIImage *stretchRightTrack = [[UIImage imageNamed:@"rightslider.png"] stretchableImageWithLeftCapWidth:5.0 topCapHeight:0.0]; [playbackSlider setMinimumTrackImage:stretchLeftTrack forState:UIControlStateNormal]; [playbackSlider setMaximumTrackImage:stretchRightTrack forState:UIControlStateNormal]; }
Obwohl dieser Code recht kompliziert aussieht, gibt er einfach nur ein paar Bilder an, die dem Schieberegler ein anderes Aussehen verleihen. Genauer, wir geben drei Bilder an: eins für den Schieber des Reglers und zwei weitere für die Anzeige der Seiten rechts und links vom Schieberegler. Sie können jetzt den Code kompilieren und ausführen, wenn Sie sich den neuen Schieberegler ansehen wollen (er funktioniert, hat natürlich aber noch keine Auswirkungen auf die anderen Bedienelemente oder das Video). Jetzt müssen wir ein paar Hausaufgaben erledigen. Wir haben einen funktionierenden Schieberegler und müssen die Minimum- und Maximumwerte für ihn in Erfahrung bringen. Wir wissen, dass der Minimumwert null Sekunden ist, aber für das Maximum benötigen wir die absolute Laufzeit des Videos. Gibt es für den entsprechenden Code einen besseren Ort als die gerade von uns geschriebene movieDurationAvailable:-Methode?
174 X Kapitel 8: Der Movie Player, Phase 2 movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.m
-(void) movieDurationAvailable:(NSNotification*)notification { if (playbackTimer == nil) { playbackTimer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(updatePlaybackTime:) userInfo:nil repeats:YES]; } X playbackSlider.minimumValue = 0.0; X playbackSlider.maximumValue = [player duration]; }
Das sollte funktionieren – sobald das Video so weit geladen ist, dass wir die Laufzeit kennen, werden Minimum- und Maximumwerte des Schiebereglers gesetzt. Nachdem wir den Wertebereich haben, müssen wir den Schieberegler dazu bringen, dass er den Wiedergabestatus des Videos widerspiegelt. Da wir bereits eine Methode haben, die den Wiedergabestatus des Videos nachhält (die Methode updatePlaybackTime:), aktualisieren wir den Wert des Schiebereglers einfach, wenn diese Methode aufgerufen wird: movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.m
-(void)updatePlaybackTime:(NSTimer*)theTimer { float playbackTime = player.currentPlaybackTime; float duration = player.duration; timeLabel.text = [NSString stringWithFormat: @"%.f von %.f Sekunden", playbackTime, duration]; playbackSlider.value = playbackTime;
X }
Dies ist wieder einmal ein guter Zeitpunkt, die Anwendung zu kompilieren und auszuführen. Jetzt sollten Sie sehen, wie der Schieberegler mitläuft, während der Film wiedergegeben wird. Allerdings können Sie den Regler noch nicht nutzen, um die Wiedergabeposition im Video anzupassen. Diese Funktionalität werden wir jetzt implementieren. Dazu müssen wir zunächst in den Interface Builder zurückkehren und zwei zusätzliche Aktionen einrichten. Geben Sie die Deklarationen für die Aktionen in die Header-Datei des Playback-Controllers ein und öffnen Sie die Player-Controller-Nib. Hier sind die Deklarationen:
Eine Videonavigation implementieren W 175 movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.h
-(IBAction)playbackSliderMoved:(UISlider *)sender; -(IBAction)playbackSliderDone:(UISlider *)sender;
Wählen Sie den UISlider -View in der Nib und öffnen Sie die Connections-Seite des Inspektors, um die Verbindungen einzurichten. Sie müssen zwei Verbindungen von dieser Seite zum File’s Owner ziehen: Verbinden Sie das Touch Up Inside-Event mit der Methode playbackSliderDone: und das Value Changed-Event mit der Methode playbackSliderMoved:. Ihre Inspektor-Seite sollte jetzt so aussehen:
Das erste Event tritt ein, wenn der Benutzer die Interaktion mit dem Schieberegler beendet und seinen Finger hebt. Das zweite Event tritt ein, wenn der Benutzer seinen Finger mit dem Schieber hin- und herbewegt. Jetzt werden wir das Verhalten für diese beiden Events implementieren. Beginnen wir mit der Methode playbackSliderMoved:. movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.m
-(IBAction)playbackSliderMoved:(UISlider *)sender { if (player.playbackState != MPMoviePlaybackStatePaused) { [player pause]; } player.currentPlaybackTime = sender.value;
176 X Kapitel 8: Der Movie Player, Phase 2 timeLabel.text = [NSString stringWithFormat: @"%.f von %.f Sekunden", sender.value, player.duration]; sliding = YES; }
Ist der Player nicht angehalten, halten wir ihn an. Würden wir den Player weiterspielen lassen, gäbe es einen dauerhaften Konflikt zwischen der Wiedergabe des Videos und der Bewegung des Schiebers. Dann setzen wir die currentPlaybackTime des Players auf den Wert des Schiebereglers und setzen damit die tatsächliche Wiedergabeposition des Players neu. Dann aktualisieren wir das Zeitlabel auf die neue Position. Schließlich führen wir eine neue Boolesche Eigenschaft namens sliding ein, die nachhält, ob der Schieber aktuell genutzt wird (vergessen Sie nicht, diese Instanzvariable in der PlaybackViewController Header-Datei zu deklarieren). Wozu wir diese Boolesche Nachverfolgung des Schiebers benötigen, werden wir gleich sehen. Schauen wir uns das alles noch einmal an, bevor wir zum nächsten Punkt weitergehen: Wird der Schieber berührt, wird das Video angehalten, und es ändert sich die Wiedergabeposition im Video, während der Schieber im Bereich zwischen seinem Minimum (0) und seinem Maximum (der Laufzeit des Videos) verschoben wird. Während das geschieht, wird das Zeitlabel des Bedienfelds aktualisiert, und es wird sichergestellt, dass die Boolesche Instanzvariable slider auf YES gesetzt wird, sobald die Interaktion mit dem Schieber beginnt. Kümmern wir uns jetzt um das, was passiert, wenn der Benutzer seinen Finger vom Schieber nimmt: movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.m
-(IBAction)playbackSliderDone:(UISlider *)sender { sliding = NO; if (player.playbackState != MPMoviePlaybackStatePlaying) { [player play]; } }
Es zeigt sich, dass diese Methode einfach ist. Wir müssen nur die Eigenschaft sliding auf NO setzen und das angehaltene Video neu starten, indem wir dem Player die play-Nachricht senden, wenn der Benutzer den Finger hebt. Und das war es auch schon. Schauen Sie sich die Sache an. (Sehen Sie, dass der Schieberegler zwar funktioniert, aber nicht ganz so, wie wir das möchten?)
Eine Videonavigation implementieren W 177
Ein weiteres Problem mit dem Schieberegler Als Sie mit dem Schieberegler experimentierten, ist Ihnen vielleicht ein weiteres Problem aufgefallen: Manchmal wird die Wiedergabe nicht wieder aufgenommen, wenn Sie Ihren Finger heben. Haben Sie eine Idee, warum das passiert? Könnte das ein UISlider-Bug sein, der bewirkt, dass das abschließende Touch Up Inside-Event nicht generiert wird? Doch nicht so hastig. Werfen Sie einen Blick auf die anderen UISlider-Events und schauen Sie, ob Sie ein eventuelles Problem sehen (und finden Sie die Lösung).
Sie wussten ja, dass wir diese Boolesche Instanzvariable nicht ohne Grund eingeführt haben. Vielleicht ist Ihnen ein etwas seltsames Verhalten des Schiebereglers aufgefallen: Wenn Sie die Position im Bereich des Schiebereglers verschieben, springt die Position sekündlich auf die letzte Position des Schiebers zurück. Warum? Weil die Methode updatePlaybackTime: sekündlich aufgerufen wird und bei jedem Aufruf den Wert des Schiebers aktualisiert. Wenn wir aber gerade dabei sind, den Schieber zu verschieben, wollen wir das nicht, und mit der Booleschen Instanzvariable sliding können Sie das jetzt beheben. So haben wir das gemacht: movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.m
X
-(void)updatePlaybackTime:(NSTimer*)theTimer { if (!sliding) { float playbackTime = player.currentPlaybackTime; float duration = player.duration; timeLabel.text = [NSString stringWithFormat: @"%.f von %.f Sekunden", playbackTime, duration]; playbackSlider.value = playbackTime;
X } }
Damit ist die Implementierung des Schiebereglers abgeschlossen. Kompilieren Sie den Code und testen Sie ihn gründlich. Jetzt wird es Zeit, die Sache abzurunden.
178 X Kapitel 8: Der Movie Player, Phase 2
8.6
Die Wiedergabesteuerung dynamisch machen Jetzt funktionieren fast alle elementaren Bestandteile der Wiedergabesteuerung, es wird also Zeit, uns anzusehen, wie das Bedienfeld selbst auf den Bildschirm kommt (statt wie bisher permanent auf diesem zu verharren). Hier ist das Verhalten, das wir gerne hätten: Wenn der Benutzer innerhalb der Grenzen des Video-Views auf den Bildschirm tippt, sollte das Bedienfeld langsam eingeblendet werden und dann offen bleiben, bis die Bedienelemente vier Sekunden nicht mehr berührt wurden. Dann soll es langsam ausgeblendet und das Video wieder vollständig sichtbar werden. Unsere erste Aufgabe besteht darin, die Implementierung so zu ändern, dass der Benutzer das Bedienfeld nicht mehr sieht, wenn die Anwendung gestartet wird. Dazu setzen wir einfach die Durchsichtigkeit auf transparent, indem wir den Alpha-Wert des View des Playback-Controllers auf 0 setzen (was auch alle Subviews transparent macht). Das kann man ganz einfach in der viewDidLoad:-Methode der Klasse PlaybackViewController erledigen. Hängen Sie lediglich unten diese Zeile an: self.view.alpha = 0.0;
Versuchen Sie, die Anwendung zu erstellen und auszuführen – Sie werden sehen, dass die Wiedergabesteuerung verschwunden ist. Keine Bange, wir zaubern sie schon wieder hervor. Jetzt wird es etwas interessanter. Sie müssen ermitteln, wann das Bedienfeld angezeigt werden muss, der Anzeige mit einer Animation etwas Pep geben und schließlich sorgfältig steuern, wann es wieder weggepackt werden soll. Wenn Sie das ordentlich machen, gibt das Ihrem Bedienfeld einen professionellen Anstrich. Machen Sie die Sache falsch, führt es zu sehr frustrierten Benutzern. Zunächst müssen wir uns darum kümmern, dass wir bemerken, wenn der Benutzer in die Videoanzeige tippt. Da der View des MPMoviePlayerController als unzugänglicher und verbotener View zu behandeln ist (in allem, was über das Hinzufügen von Subviews hinausgeht), werden wir einen eigenen UIView namens touchView erstellen, ihn über das Video legen (worin wir mittlerweile äußerst versiert sind) und dann Taps auf diesem View festhalten. Hängen Sie dazu ans Ende der viewDidLoad:-Methode des PlaybackViewController folgenden Code an:
Die Wiedergabesteuerung dynamisch machen W 179 movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.m
UIView *touchView = [[[UIView alloc] init] autorelease]; touchView.frame = player.view.frame; [player.view addSubview:touchView];
Fügen Sie dem View dann eine einfache Ein-Tap-Geste hinzu: movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.m
UITapGestureRecognizer *tapRecognizer = [[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapFrom:)] autorelease]; [tapRecognizer setNumberOfTapsRequired:1]; [touchView addGestureRecognizer:tapRecognizer];
Hier haben wir einen Recognizer für Tap-Gesten erstellt, der handleTapFrom: als Aktion nutzt, und ihn an den touchView gebunden. Schreiben wir jetzt die Methode handleTapFrom:: movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.m
-(void)handleTapFrom:(UITapGestureRecognizer *)recognizer { [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:1.5]; self.view.alpha = 1.0; [UIView commitAnimations]; [self setControlsTimer]; }
Die handleTapFrom:-Methode erfüllt zwei Funktionen: Zunächst richtet sie eine Animation ein, die den View des PlaybackViewController von einem alpha-Wert von 0 über die Dauer von anderthalb Sekunden zum Wert 1 führt. Das bewirkt einen netten Einblendeeffekt für unsere Wiedergabesteuerung. Die zweite Aufgabe der Methode handleTapFrom: ist die Einrichtung eines Timers, der nach vier Sekunden ausgelöst wird. Zur Einrichtung des Timers ruft sie die Methode setControlsTimer: movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.m
-(void)setControlsTimer { if (controlsTimer) { [controlsTimer invalidate]; [controlsTimer release]; controlsTimer = nil; } controlsTimer = [[NSTimer timerWithTimeInterval:4.0 target:self selector:@selector(handleControlsTimer:) userInfo:nil repeats:NO] retain];
180 X Kapitel 8: Der Movie Player, Phase 2 [[NSRunLoop currentRunLoop] addTimer:controlsTimer forMode:NSDefaultRunLoopMode]; }
Diese Methode wirkt vielleicht ein wenig komplizierter als erwartet. Gehen wir sie im Einzelnen durch. Zunächst prüfen wir hier eine neue Eigenschaft, controlsTimer, die, wenn sie einen aktiven Timer referenziert, erst ungültig gemacht und dann freigegeben wird. Warum dieser Code an dieser Stelle steht? Betrachten Sie das so: Wenn ein Benutzer in den Videobereich tippt, soll ein Timer eingerichtet werden, der in vier Sekunden ausgelöst wird, aber wenn der Benutzer erneut tippt, gibt es bereits einen aktiven Timer, den wir deswegen ungültig machen müssen. Weiter im Code: Unabhängig davon, ob es einen aktiven Timer gibt oder nicht, müssen wir einen weiteren Timer erstellen, der in vier Sekunden ausgelöst wird. Das macht der verbleibende Code in dieser Methode, der die Methode handleControlsTimer: als Selektor nutzt, wenn der Timer ausgelöst wird. Schreiben wir nun die Methode handleControlsTimer:, die ihre Verantwortung eigentlich nur an removeControls: weitergibt. Beide sind hier aufgeführt: movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.m
-(void)handleControlsTimer:(NSTimer *)timer { [self removeControls]; [controlsTimer release]; controlsTimer = nil; } movieplayer/MoviePlayer6Start/Classes/PlaybackViewController.m
-(void)removeControls { [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:1.5]; self.view.alpha = 0.0; [UIView commitAnimations]; }
Die Methode removeControls: verbirgt die Wiedergabesteuerung einfach auf gleiche Weise, wie sie zuvor eingeblendet wurde, aber in umgekehrter Richtung: Der Alpha-Wert wird in einem Animationsblock wieder auf 0 gesetzt, was einen hübschen Ausblendeeffekt bewirkt. Wir sind noch nicht ganz fertig. Kompilieren Sie den Code dennoch und testen Sie ihn. Sie werden sehen, dass das Bedienfeld ordentlich erscheint, aber immer nach vier Sekunden verschwindet – was nicht ganz das gewünschte Verhalten war. Eigentlich sollte es angezeigt werden, solange es Aktivitäten gibt und der Benutzer es auf irgendeine Weise berührt.
Der Vollbildmodus Aber das können wir leicht beheben. Wir müssen einfach folgende Zeile an einige ausgesuchten Orten einfügen: [self setControlsTimer];
Wir haben diesen Code ans Ende der Methoden playbackSliderMoved: und handle-PlayAndPauseButton: angehängt. Das sorgt dafür, dass der Timer zurückgesetzt wird, wenn der Play-/Pause-Button oder der Schieberegler berührt wird. Fügen Sie einen setControlsTimer:-Aufruf ein und testen Sie die Anwendung – jetzt sollte alles wie erwartet funktionieren.
Bonusaufgabe: Ein kleines Detail Bei einigen Video-Playern ist es üblich, dass die Wiedergabesteuerung geöffnet bleibt, solange der Pause-Button gedrückt ist. Bei unserer Implementierung verschwindet das Bedienfeld nach vier Sekunden, wenn Sie den Pause-Button drücken (probieren Sie es aus). Wie würden Sie die Implementierung ändern, um das Bedienfeld offen zu lassen, bis der Play-Button (oder ein anderer Button) gedrückt wird? Denken Sie darüber nach und probieren Sie es selbst, bevor Sie sich die Lösung ansehen, die Sie hier unten sehen: movieplayer/MoviePlayer6Full/Classes/PlaybackViewController.m
X
X X X
8.7
-(IBAction)handlePlayAndPauseButton:(id)sender { UIButton *button = (UIButton *)sender; if (button.selected) { button.selected = NO; [player play]; [self setControlsTimer]; } else { button.selected = YES; [player pause]; [controlsTimer invalidate]; [controlsTimer release]; controlsTimer = nil; } }
Der Vollbildmodus In Apps eingebettete Videos funktionieren auf dem iPad wunderbar, meinen Sie nicht auch? Aber noch besser ist eine Vollbildanzeige. Wird es daher nicht Zeit, dass wir uns an diesem Vollbild-Button versuchen? Fügen Sie dazu der Header-Datei PlaybackViewController.h eine IBAction namens handleFullscreenButton hinzu und verbinden Sie dann
W
181
182 X Kapitel 8: Der Movie Player, Phase 2 im Interface Builder den Vollbild-Button in der Wiedergabesteuerung mit dieser Aktion. Danach können Sie den Vollbildmodus einfach folgendermaßen mit einem Aufruf der Methode setFullscreen:animated einschalten: movieplayer/MoviePlayer6Full/Classes/PlaybackViewController.m
-(IBAction)handleFullscreenButton { [player setFullscreen:YES animated:YES]; }
Wenn Sie gleich den nächsten Schritt gemacht, nämlich den Code kompiliert und ausgeführt haben, ist Ihnen wahrscheinlich ein kleines Problem aufgefallen: Wenn Sie sich im Vollbildmodus befinden, gibt es kein Bedienfeld mehr. Warum? Wir haben unser Bedienfeld dem Subview des App-internen View hinzugefügt. Wechselt der Player in den Vollbildmodus, nutzt er dazu einen anderen View. Wollen wir unser Bedienfeld in diesen View bewegen, müssen wir uns Zugriff auf den mainScreen des iPads verschaffen, etwas, das wir in Kapitel 10, Externe Anzeigegeräte nutzen, tun werden. Statt uns jetzt schon auf diesen Pfad zu begeben, wollen wir uns hier genauer mit der Fullscreen-API befassen und Notifikationen nutzen, um die Standardbedienelemente zu aktivieren, wenn das Video im Vollbildmodus angezeigt wird. Beginnen wir damit, dass wir uns in der initWithPlayer:-Methode der Datei PlaybackViewController.m für zwei Notifikationen registrieren. Hängen Sie einfach ans Ende dieser Methode folgenden Code an: movieplayer/MoviePlayer6Full/Classes/PlaybackViewController.m
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerWillExitFullscreen:) name:MPMoviePlayerWillExitFullscreenNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerDidEnterFullscreen:) name:MPMoviePlayerDidEnterFullscreenNotification object:nil];
Definieren wir jetzt die Methode playerDidEnterFullscreen:. In dieser Methode müssen wir den Stil der Player-Bedienelemente auf einen Stil wie MPMovieControlStyleDefault setzen, der ein Bedienfeld und eine Möglichkeit einschließt, den Vollbildmodus zu beenden:
Zusammenfassung W 183 movieplayer/MoviePlayer6Full/Classes/PlaybackViewController.m
-(void) playerDidEnterFullscreen:(NSNotification*)notification { player.controlStyle = MPMovieControlStyleDefault; }
Aber wenn wir den Vollbildmodus verlassen, dürfen wir nicht gleichzeitig unser eigenes Bedienfeld und den Standard-Control-Stil haben, weil wir dann zwei konkurrierende Bedienfelder haben. Nutzen wir die andere Notifikation, um den Bedienfeldstil wieder zurückzusetzen: movieplayer/MoviePlayer6Full/Classes/PlaybackViewController.m
-(void) playerWillExitFullscreen:(NSNotification*)notification { player.controlStyle = MPMovieControlStyleNone; }
Kompilieren Sie den Code und führen Sie ihn aus. Sie sollten jetzt eine vollständig funktionsfähige Vollbildanzeige haben.
8.8
Zusammenfassung Wenn Sie die letzten beiden Kapitel noch einmal Revue passieren lassen, werden Sie feststellen, dass wir uns mit einer Menge Dinge befasst haben. Sie sind jetzt dazu in der Lage, eine eigene ausgefeilte Videodarstellung zu bieten, und können Videos gut integriert und interaktiv in Ihre Anwendung einbetten. Es fehlt nur noch ein einziger Baustein: Video, das aus dem Internet gestreamt wird, und das werden wir uns jetzt vornehmen – im nächsten Kapitel.
Kapitel 9
Apples HTTP-LiveStreaming Wie Sie in den letzten beiden Kapiteln sahen, ist es recht leicht, Videos aus dem Application-Bundle zu spielen (und es gibt bestimmt Anwendungen, bei denen eine derartige Einbettung der Videoinhalte zweckdienlich ist). Dass wir eingeschlossene Videos verwendet haben, erlaubte uns, uns auf die Gestaltung des Videokonsums zu konzentrieren, ohne dass wir uns mit Dingen wie Netzwerkverbindungen, Servern, Latenzzeiten oder Bandbreite herumschlagen mussten. Aber bei vielen Anwendungen, wenn nicht gar den meisten, müssen Sie Video über das Netzwerk auf das iPad streamen. In diesem Kapitel werden wir uns iPad-Video-Streaming ansehen – warum Sie es brauchen, was es bedeutet, wie es sich von der Wiedergabe lokaler Videos unterscheidet und wie man es bewerkstelligt. Sie werden dabei – mit wenigen Ausnahmen – alles einbringen können, was Sie in den letzten beiden Kapiteln gelernt haben, aber wir werden auch einige neue Dinge einführen, die Sie benötigen, wenn Sie Videos über das Netzwerk befördern wollen.
9.1
Progressives Video vs. Streamed-Video Beginnen wir zunächst mit einer weitverbreiteten Verwirrung: progressives vs. gestreamtes Video. Bei progressiv ausgelieferten Videos veröffentlichen Sie eine Videoressource auf einem Webserver und fordern den Client auf, diese über das HTTP-Protokoll abzurufen. Bei dieser Methode lädt der Media-Player die Ressource herunter und gibt sie wie-
186 X Kapitel 9: Apples HTTP-Live-Streaming der (üblicherweise in Form einer lokal zwischengespeicherten Datei), wenn er den Inhalt erhält. Progressives Herunterladen hat seine Vorteile. Es ist einfach und basiert darauf, dass die Videodatei auf einem beliebigen Webserver bereitgehalten wird. Es hat aber auch seine Nachteile: Es kann verschwenderisch sein (häufig wird erheblich mehr Inhalt heruntergeladen, als der Benutzer je betrachtet) und bietet keine Möglichkeit, die Bitrate des Videos anzupassen, während der Benutzer es betrachtet. Außerdem belegt Apple Ihre Anwendungen mit Einschränkungen – wenn das Video länger als zehn Minuten ist oder innerhalb von fünf Minuten mehr als 5 MByte Daten erfordert, müssen Sie Streaming statt progressiven Herunterladens nutzen. Warum? Das Streamen von Videos vermeidet einige der Probleme des progressiven Herunterladens, die wir gerade erwähnt haben, und ermöglicht uns, die Bitrate nach Bedarf anzupassen, wenn sich die Netzwerkbedingungen der Verbindung des Benutzers ändern. Die Nachteile? Üblicherweise benötigte man eine proprietäre Infrastruktur und proprietäre Protokolle, um Streaming zu implementieren. Das hat Apple mit seinem HTTP-basierten StreamingProtokoll verbessert. Schauen wir uns die Sache mal an.
9.2
Apples HTTP-basiertes Streaming-Protokoll Apple hat ein auf offenen Standards basierendes Protokoll namens HTTP-Live-Streaming (kurz HLS) geschaffen. Da Apple das Protokoll auf HTTP basieren lässt, dem Standardprotokoll, das von allen Browsern und Webservern genutzt wird, können Sie Ihre Videoinhalte auf irgendeinem Standard-Webserver bereithalten, um sie auf das iPad zu streamen. Und lassen Sie sich nicht von dem “Live” im Namen HLS verwirren. Obwohl HLS das Streaming von Live-Inhalten wie Sportereignissen oder Konzerten ermöglicht (mit Unterstützung von kommerziell auf dem Markt verfügbaren Echtzeit-Kodierern), funktioniert es auch bei Video-on-Demand-basierten Anwendungen, bei denen Sie mit zuvor aufgezeichneten Videoinhalten arbeiten, die Sie Ihrem Benutzern zukommen lassen wollen.
Wie HTTP-Live-Streaming funktioniert Die meisten kommerziellen Streaming-Lösungen transportieren Ihren Videoinhalt unter Verwendung eines proprietären Protokolls über ein oder mehrere offene Sockets. Die Arbeit mit diesen proprietären Lösungen verlangt üblicherweise, dass Sie große Serverfarmen einrichten
Apples HTTP-basiertes Streaming-Protokoll und verwalten, die speziell für das jeweilige Streaming-Protokoll gedacht sind (und häufig in Content-Delivery-Netzwerken bereitgestellt werden). HLS nutzt einen anderen Ansatz: Das Video wird in mehrere kleine Segmente aufgeteilt und dann auf gewöhnlichen Webservern gespeichert. Video-Player auf dem Client nutzen das übliche HTTP-Protokoll, um die Segmente anzufordern und zu empfangen, die dann zusammengenäht werden, um eine nahtlose Videowiedergabe zu ermöglichen. Der HLS-Ansatz hat einige wichtige Vorteile: Wie wir bereits sagten, können die Inhalte auf einem gewöhnlichen Webserver gespeichert und von ihm ausgeliefert werden. Aufgrund der Allgegenwärtigkeit des HTTP-Protokolls werden außerdem viele der möglichen Netzwerkprobleme vermieden, die uns bei proprietären Protokollen mit Routern und Firewalls begegnen. Zusätzlich gibt es einen weiteren großen Vorteil: Da das Video in Segmente aufgeteilt ist, kann der Player jedes Mal, wenn er ein paar Segmente abrufen muss, entscheiden, welche Bandbreite verfügbar ist, und dementsprechend Segmente anfordern, die mit einer höheren bzw. geringeren Bitrate kodiert wurden (wenn diese verfügbar sind). So kann die Qualität der Videoauslieferung verbessert werden.
Videos segmentieren Wie zu erwarten war, führt das Segmentieren von Videos zu zusätzlicher Arbeit, weil Sie Ihr Video zunächst in kleine Happen zerlegen müssen (die üblicherweise zehn Sekunden lang sind), damit es von HLS genutzt werden kann. Wenn Sie zusätzlich das Wechseln der Bitrate unterstützen wollen, müssen Sie das für alle Ressourcen mehrfach tun (jeweils einmal für alle Bitraten, die Sie unterstützen wollen). Das Segmentieren von Videos übersteigt den Horizont dieses Buchs. Aber Apple bietet viele Werkzeuge und Ressourcen, die Sie dabei unterstützen. Sehen Sie sich den HTTP Live Streaming Overview [App10e] an, der Verweise auf alle Werkzeuge und Hintergrundinformationen enthält, die Sie benötigen, um Ihre Videos für HLS vorzubereiten. Aber auch wenn wir das Segmentieren hier nicht ausführlich betrachten wollen, müssen Sie in gewissem Maße mit dem vertraut sein, was Apples Segmentierungswerkzeug ausgibt, wenn Sie verstehen wollen, wie der Client das Video vom Webserver abruft.
W
187
188 X Kapitel 9: Apples HTTP-Live-Streaming
.ts-Segmente
AppleSegmentierer HTTP Manifest Abbildung 9.1: Apples HTTP-Live-Streaming Der Segmentierungsprozess wird in Abbildung 9.1 dargestellt – der Segmentierer nimmt eine Videoressource und erzeugt einen Satz von .tsDateien (die Segmente) und eine Manifestdatei (die gelegentlich auch als Playlist bezeichnet wird – und nicht mit den Wiedergabelisten verwechselt werden sollte, die wir in Kapitel 7, Der Movie Player, implementiert haben). HLS-Manifestdateien werden als M3U8-Dateien bezeichnet und sehen so aus: #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:10, no desc fileSequence0.ts #EXTINF:10, no desc fileSequence1.ts #EXTINF:10, no desc fileSequence2.ts #EXTINF:10, no desc fileSequence3.ts . . . #EXTINF:1, no desc fileSequence180.ts #EXT-X-ENDLIST
Die Manifestdatei hat die Aufgabe, alle Videosegmente zu beschreiben, die für eine bestimmte Videoressource generiert wurden (bei LiveVideo-Anwendungen ist die Verwendung etwas anders, aber auch diese gehen über den Horizont dieses Buchs hinaus). Gehen wir die Datei durch. Die erste Zeile identifiziert den Dateityp. Die nächste Zeile, die Ziel-Laufzeit, gibt an, wie lang die einzelnen Segmente sind (hier zehn Sekunden), während die dritte Zeile eine Sequenznummer angibt, die für das Live-Streaming genutzt wird. Anschließend folgen der Reihe nach die Segmente (um Platz zu sparen,
Apples HTTP-basiertes Streaming-Protokoll haben wir rund 180 dieser Beschreibungen weggelassen) unter Angabe der Dauer, einer optionalen Beschreibung und schließlich einem Ressourcennamen für das Segment. Dieses Manifest sorgt dafür, dass der Media-Player zuerst fileSequence0.ts, dann fileSequence1.ts und so weiter abruft. Der Player selbst kümmert sich um alle Timing-Aspekte des Abrufens der Segmente und um das Zusammenfügen, das eine nahtlose Wiedergabe ermöglicht. Eine Manifestdatei wie die obige würde üblicherweise für jede Bitrate erstellt, die unterstützt werden soll. Wir benötigen also eine weitere Datei, die alle verfügbaren möglichen Segmente (nach Bitrate) beschreibt. Diese Datei sieht so aus: #EXTM3U #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=200000 gear1/prog_index.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=311111 gear2/prog_index.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=484444 gear3/prog_index.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=737777 gear4/prog_index.m3u8
Diese M3U8-Datei führt vier mögliche Ressourcen auf, eine, die mit 200Kbps kodiert und über die Manifestdatei gear1/prog_index.m3u8 verfügbar ist, eine weitere, die mit 311Kbps kodiert und in der Datei gear2/prog_index.m3u8 verfügbar ist und so weiter. Verweisen Sie den Player auf diese Datei, verarbeitet dieser sie, um die verfügbaren Bitraten zu ermitteln, damit er auf Basis der verfügbaren Bandbreite festlegen kann, welche Ressource als Nächstes abgerufen werden soll. Beachten Sie, dass der Player beim ersten Inhalt, den er abruft, immer mit der ersten Datei beginnt, die in der Manifestdatei angegeben ist, und dann auf Basis der Netzwerkverbindung prüft, was er bei späteren Abrufvorgängen nutzen soll.
Die weiteren Schritt mit HLS Ihnen ist vermutlich klar, dass wir den Segmentierungsvorgang und die Konfiguration des HLS-Backends nur gestreift haben. Wenn Sie ernsthaft mit HLS arbeiten wollen, müssen Sie noch eine Menge Dinge lernen. Aber natürlich sind CDN und andere Anbieter gern bereit, Ihnen die meisten Mühen abzunehmen, wenn Sie sie Ihre HLS-Inhalte hosten lassen. Nachdem wir die Grundlagen der Segmentierung beschrieben haben, wollen wir jetzt aber zum eigentlichen Thema dieses Kapitels übergehen – wie man Streaming auf der Clientseite unterstützt.
W
189
190 X Kapitel 9: Apples HTTP-Live-Streaming
9.3
Einen Streaming-Player erstellen Wie Sie sehen werden, ist es nach all den Vorarbeiten in den letzten beiden Kapiteln recht einfach, einen Streaming-Player in Betrieb zu nehmen. Wir werden zu MoviePlayer3 zurückkehren und dort neu ansetzen – tatsächlich haben wir schon eine entsprechend neue Version namens MoviePlayer3Streamed erstellt. Wir werden Sie bitten, eine kleine Änderung in der Datei MoviePlayerViewController.m vorzunehmen. Suchen Sie die Methode movieURL, löschen Sie den gesamten enthaltenen Code und ersetzen Sie ihn durch folgenden Code: movieplayer/MoviePlayer3Streamed/Classes/MoviePlayerViewController.m
-(NSURL *)movieURL { return [NSURL URLWithString: @"http://devimages.apple.com/iphone/samples/bipbop/gear1/prog_index.m3u8" ]; }
Bevor wir uns diesem neuen Code zuwenden, sollten Sie bedenken, dass der Code, den Sie gerade gelöscht haben, eine NSURL erzeugte, die auf eine Ressource im Bundle verwies. Hier erstellen Sie stattdesssen eine URL, die auf eine M3U8-Datei weist, die auf Apples Servern bereitgehalten wird. Nehmen Sie diese Änderung vor, kompilieren den Code und führen Sie ihn aus (prüfen Sie zunächst, ob eine Netzwerkverbindung besteht). Sie sollten, nach einer kleinen Phase der Pufferung, ein Testvideo wie das in Abbildung 9.2 sehen. Beachten Sie, dass die Bitrate dieses Videos recht gering ist (werfen Sie dazu einen Blick auf die Qualität des Videos). Ändern Sie dann die URL in Folgendes: http://devimages.apple.com/iphone/samples/bipbop/gear2/prog_index.m3u8
Ist die Qualität jetzt besser? (Wir setzen voraus, dass Ihre Bandbreite für diese Bitrate ausreicht.) Probieren wir es noch einmal – nutzen Sie diese URL: http://devimages.apple.com/iphone/samples/bipbop/gear4/prog_index.m3u8
Noch besser? Gut, dann verweisen wir den Player jetzt auf eine M3U8Datei, die auf alle drei Ressourcen zeigt, damit er selbst entscheiden kann, welche am besten ist: http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8
Auf die Netzwerkumgebung reagieren W 191
Abbildung 9.2: Ein Streaming-Testbild Nun sollten Sie sehen, dass der Stream zunächst mit der geringsten Qualität abgespielt und später dann angepasst wird. Wenn Sie hinreichend Bandbreite haben, sehen Sie nach zehn Sekunden (oder ein paar mehr Sekunden, wenn der Player bereits mehr Inhalt gepuffert hat) die Version mit der höchsten Auflösung.
9.4
Auf die Netzwerkumgebung reagieren Eine angenehme Eigenschaft von Apples Implementierung ist, dass sie sich um die Einzelheiten der Bitratenänderung kümmert, wenn sich die Netzwerkbedingungen ändern. Dennoch kann es vorkommen, dass die Netzwerkbandbreite gelegentlich nicht ausreicht und die Videowiedergabe hängen bleibt. Wenn das passiert können Sie die MPMoviePlayerLoadStateDidChangeNotification-Notifikation nutzen, um ein anwendungsspezifisches Verhalten anzubieten. Schreiben wir etwas Code, um damit zu experimentieren: Registrieren Sie sich zunächst für die Notifikation in der viewDidLoad-Methode und geben Sie handleLoadStateDidChange: als Selektor an.
192 X Kapitel 9: Apples HTTP-Live-Streaming movieplayer/MoviePlayer3Streamed/Classes/MoviePlayerViewController.m
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleLoadStateDidChange:) name:MPMoviePlayerLoadStateDidChangeNotification object:nil];
Bevor Sie die Methode handleLoadStateDidChange definieren, fügen Sie der MoviePlayerViewController.xib-Nib einen UIActivityIndicatorView hinzu. Ziehen Sie einen UIActivityIndicatorView auf den HauptNib-View. Wir geben unserem den Stil Large White und klicken auch auf “Hide when stopped.” Fügen Sie außerdem MoviePlayerViewController.h ein IBOutlet hinzu und geben Sie diesem den Namen activityView. Stellen Sie dann im Interface Builder die Verbindung her.
Wie erfahre ich, welche Bitrate der Player nutzt? Unglücklicherweise bietet Apple keine Möglichkeit, die Bitrate zu ermitteln, die aktuell genutzt wird. Nachdem Sie die Inhalts-URL des Players auf eine M3U8-Datei gesetzt haben, kümmert sich der Player um die gesamte Logik und bietet keine Informationen dazu, wie zwischen den verfügbaren Streams umgeschaltet wird. Sie können sich allerdings benachrichtigen lassen, wenn sich der Ladezustand des Inhalts ändert (das Video beispielsweise hängen bleibt).
Nachdem wir das erledigt haben, ist es fast so weit, dass wir die Methode handleLoadStateDidChange: definieren können. Erst müssen wir allerdings etwas mehr über Ladezustände wissen. Die Ladezustände sind als enum definiert und umfassen Folgendes: MPMovieLoadStateUnknown
Der Ladezustand ist unbekannt. MPMovieLoadStatePlayable
Der Player ist in einer Verfassung, in der Videos wiedergegeben werden, ihm aber der Pufferspeicher ausgehen kann. MPMovieLoadStatePlaythroughOK
Der Player ist wiedergabebereit und hat hinreichend Puffer, um die Wiedergabe zu starten. MPMovieLoadStateStalled
Der Player hängt.
Zusammenfassung W 193 Gut, machen wir uns also an die Definition von handleLoadStateDidChange: movieplayer/MoviePlayer3Streamed/Classes/MoviePlayerViewController.m
- (void) handleLoadStateDidChange:(NSNotification*)notification { MPMovieLoadState state = [player loadState]; if (state & MPMovieLoadStateStalled) { [activityView startAnimating]; } else if (state & MPMovieLoadStatePlaythroughOK) { [activityView stopAnimating]; } }
Hier verschaffen wir uns zunächst den Ladezustand vom Player. Wir interessieren uns für zwei Zustände, MPMovieLoadStateStalled und MPMovieLoadStatePlaythroughOK. Ist der Ladezustand MPMovieLoadStateStalled, wollen wir den Aktivitätsindikator animieren, weil das Video hängt, und dem Benutzer anzeigen, dass im Hintergrund eine Pufferungsaktivität erfolgt. Und wenn der Zustand MPMovieLoadStatePlaythroughOK ist, soll die Animation des Indikators beendet werden, weil das Video erneut wiedergegeben wird. Geben Sie diesen Code ein, kompilieren Sie ihn und führen Sie ihn aus. Das Testen kann sich als etwas kompliziert erweisen, weil Sie eine unzuverlässige Netzwerkverbindung simulieren müssen. Häufig erreichen Sie das, indem Sie die Ethernet-Verbindung trennen (entweder am WLAN-Router oder, wenn Sie den Simulator nutzen, an Ihrem Mac). Das war’s! Jetzt sind wir alle Details des Video-Streamings auf der Clientseite durchgegangen.
9.5
Zusammenfassung Wir haben uns gerade die Grundlagen des Video-Streamings angesehen, das Ihnen, gemeinsam mit den Werkzeugen, die wir in den beiden vorangegangenen Kapiteln behandelt haben, eine große Hilfe sein wird, wenn Sie eine überzeugende videobasierte Anwendung schaffen wollen. Das soll nicht heißen, dass wir sämtliche Details behandelt haben, beispielsweise den Stream-Wechsel mitten in der Wiedergabe (oder das Einstreuen von Werbung), für deren Umsetzung Sie sich tief in die Einzelheiten der Segmentierung und des HLS-Protokolls einarbeiten müssen. Sie sollten auch beachten, dass HLS ein junges Protokoll ist und Protokoll sowie die entsprechende Implementierung, MPMoviePlayerController, in den kommenden Versionen sicher noch wachsen und sich verändern werden.
Kapitel 10
Externe Anzeigegeräte nutzen Apple bietet eine Reihe von Adapterkabeln wie den iPad Dock Connector auf VGA Adapter (Komposit- und Component-Versionen sind auch verfügbar), über die Sie das iPad mit einem externen Anzeigegerät, beispielsweise mit einem Beamer oder einem Monitor, verbinden können. Man könnte, vor allem nachdem man gesehen hat, wie Steve Jobs das iPad in seinen Keynotes vorgestellt hat, erwarten, dass dieser Adapter eingesetzt wird, um den iPad-Bildschirm auf einem externen Schirm zu spiegeln. Leider ist das nicht der Fall, und was auch immer Steve dazu auf seinem iPad nutzt, für uns ist das nicht verfügbar. Aber für viele Anwendungen ist eine Spiegelung ohnehin nicht die beste Wahl. Denken Sie beispielsweise an eine Präsentationsanwendung, in der Sie Ihrem Publikum auf dem externen Bildschirm die Folien anzeigen, während Sie auf dem eigentlichen Bildschirm auf Ihre Notizen blicken. In diesem Kapitel werden Sie lernen, wie Sie Inhalte, die auf einem externen Bildschirm dargestellt werden, in eine Anwendung integrieren. Dazu müssen Sie zunächst wissen, wie man einen externen Bildschirm erkennt, eine Referenz auf das Bildschirm-Objekt erhält und auf ihm Views erstellt. Sie müssen auch überlegen, wie Sie diesen externen Bildschirm nutzen wollen (und denken Sie ebenfalls daran, dass es keine Multi-Touch-Anzeige ist, mit der der Benutzer unmittelbar interagieren kann). Wir werden mit einer einfachen Anwendung beginnen, die nach einem externen Anzeigegerät sucht, und an diesem Punkt beginnend dann den ganzen Weg bis zur Anzeige von Videos im Vollbildmodus gehen.
196 X Kapitel 10: Externe Anzeigegeräte nutzen
10.1
Ein externes Anzeigegerät erkennen Bevor Sie auf einem externen Monitor Inhalte anzeigen können, müssen Sie in Erfahrung bringen, ob überhaupt ein zweiter Bildschirm mit dem iPad verbunden ist. Die Klasse UIScreen bietet zwei Notifikationen, UIScreenDidConnectNotification und UIScreenDidDisconnectNotification, die abgesetzt werden, wenn ein externes Anzeigegerät mit dem iPad verbunden bzw. vom iPad getrennt wird. Erstellen wir eine einfache View-basierte Anwendung, die erkennt, wenn wir einen externen Bildschirm anschließen bzw. entfernen.
Abbildung 10.1: Ein Statuslabel einrichten Erstellen Sie zunächst in Xcode eine View-basierte Anwendung namens ExternalDisplay. Öffnen Sie dann die ExternalDisplayViewController.xib-Nib und fügen Sie ihr zwei Labels hinzu, wie Sie es in Abbildung 10.1 sehen. Fügen Sie dann in die Header-Datei ExternalDisplayViewController.h ein Outlet namens statusLabel ein und verbinden Sie es
im Interface Builder mit dem Statuslabel (dem Label auf der rechten Seite). Öffnen Sie dann die Klasse ExternalDisplayViewController, heben Sie die Auskommentierung der vorhandenen viewDidLoad-Methode auf
Ein externes Anzeigegerät erkennen W 197 und fügen Sie ihr folgenden Code hinzu, um sich für den Empfang der beiden UIScreen-Notifikationen zu registrieren: ExternalDisplay/ExternalDisplay1/Classes/ExternalDisplayViewController.m
- (void)viewDidLoad { [super viewDidLoad]; NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter addObserver:self selector:@selector(screenDidConnectNotification:) name:UIScreenDidConnectNotification object:nil]; [notificationCenter addObserver:self selector:@selector(screenDidDisconnectNotification:) name:UIScreenDidDisconnectNotification object:nil];
X X X X X X X X X X }
Hier beschaffen wir uns eine Referenz auf das NSNotificationCenter und nutzen sie, um uns für die UIScreenDidConnectNotification-Notifikation zu registrieren, die, wenn sie gesendet wird, den Aufruf der Methode screenDidConnectNotification: veranlasst. Gleichermaßen sorgen wir dafür, dass das Senden einer UIScreenDidDisconnectNotification bewirkt, dass die Methode screenDidDisconnectNotification: aufgerufen wird. Schreiben wir nun diese beiden Methoden: ExternalDisplay/ExternalDisplay1/Classes/ExternalDisplayViewController.m
-(void)screenDidConnectNotification:(NSNotification *)notification { statusLabel.text = @"Connected"; } ExternalDisplay/ExternalDisplay1/Classes/ExternalDisplayViewController.m
-(void)screenDidDisconnectNotification:(NSNotification *)notification { statusLabel.text = @"Nicht verbunden"; }
Viel passiert hier nicht. Wir aktualisieren einfach den Status des statusLabel, wenn ein externes Anzeigegerät angeschlossen bzw. wieder entfernt wird. Eine Sache sollten wir allerdings betonen, damit Ihnen das zukünftig bewusst ist: Das UIScreenDid-ConnectNotification-Objekt zeigt auf das neu eingebundene UIScreen-Objekt. Zugriff auf dieses neue UIScreen-Objekt können wir uns also folgendermaßen verschaffen: UIScreen *externalScreen = (UIScreen *)[notification object];
198 X Kapitel 10: Externe Anzeigegeräte nutzen Darauf werden wir in Kürze zurückkommen. Zunächst können wir aber unseren bisherigen Code testen. Kompilieren Sie den Code und installieren Sie ihn auf Ihrem iPad. Lösen Sie das Gerät dann von Ihrem Entwicklungsrechner (sollte die Anwendung auf dem Gerät laufen, bewirkt das, dass sie beendet wird). Berühren Sie jetzt das Anwendungssymbol, um die Anwendung neu zu starten, stecken Sie das Adapterkabel ein und ziehen Sie es wieder heraus. Sie werden sehen, dass sich der Text im Label ändert, wenn Sie das tun. Nachdem wir gesehen haben, dass das funktioniert, sollten wir uns über eine weitere Sache Gedanken machen: Was passiert, wenn bereits ein externer Bildschirm angeschlossen ist, wenn die Anwendung gestartet wird? Beim oder nach dem Start der Anwendung wird keine Notifikation gesendet (zumindest so lange, bis jemand das Kabel herauszieht). Das Statuslabel wird also „Nicht verbunden“ anzeigen, obwohl es eigentlich „Verbunden“ sagen sollte. Um auf diesen Fall vorbereitet zu sein, werden wir beim Start der Anwendung manuell prüfen, ob ein zweiter Bildschirm angeschlossen ist. Wie? Das werden wir über die Klasse UIScreen tun, die zwei Klassenmethoden bietet, die wir nutzen können: screens und mainScreen. Die Methode mainScreen liefert das UIScreen-Objekt, das den Hauptbildschirm des Geräts repräsentiert. Die Methode screens hingegen liefert ein Array mit UIScreen-Objekten, einem für jedes angeschlossene Anzeigegerät. Beim iPad kann es mit den aktuell verfügbaren Adaptern immer nur einen angeschlossenen Bildschirm geben.1 Schreiben wir unter diesen Voraussetzungen Code, der prüft, ob es einen zweiten Bildschirm gibt, und, wenn das der Fall ist, das entsprechende UIScreen-Objekt liefert: ExternalDisplay/ExternalDisplay1/Classes/ExternalDisplayViewController.m
-(UIScreen *)scanForExternalScreen { NSArray *deviceScreens = [UIScreen screens]; for (UIScreen *screen in deviceScreens) { if (![screen isEqual:[UIScreen mainScreen]]) { return screen; } } return nil; }
Hier nutzen wir zunächst die Klassenmethode screens, die ein Array mit den verbundenen Anzeigegeräten liefert. Beim iPad ist das, wenn kein externes Anzeigegerät angeschlossen ist, ein Array der Länge eins 1
Das könnte sich natürlich nach Drucklegung dieses Buchs ändern.
Ein externes Anzeigegerät erkennen W 199 mit einem Element, das eine Referenz auf den Hauptbildschirm darstellt. Ist ein externes Anzeigegerät angeschlossen, hat das Array die Länge zwei. Dann durchlaufen wir das Array und suchen nach einem Anzeigegerät, das nicht der Hauptbildschirm ist – zum Vergleich nutzen wir das Objekt, das von der Klassenmethode mainScreen geliefert wird. Wenn wir ein Anzeigegerät finden, das nicht der Hauptbildschirm ist, liefern wir dieses unmittelbar zurück. Finden wir keins, wird nil geliefert. Nachdem wir diese Methode haben, können wir sie beim Start der Anwendung nutzen, um den richtigen Status für unser statusLabel zu bestimmen (verbunden oder nicht verbunden). Zunächst müssen Sie dazu der Klasse ExternalDisplayViewController eine Eigenschaft des Typs UIScreen namens externalScreen hinzufügen, die eine Referenz auf das Anzeigegerät aufnehmen kann. Fügen Sie dann Ihrer viewDidLoad-Methode folgenden Code hinzu: ExternalDisplay/ExternalDisplay1/Classes/ExternalDisplayViewController.m
externalScreen = [self scanForExternalScreen]; if (externalScreen) { statusLabel.text = @"Verbunden"; }
Wenn wir die Anwendung starten, prüft viewDidLoad: jetzt, ob bereits ein anderes Display angeschlossen ist, und setzt unser Statuslabel gegebenenfalls auf „Verbunden“. Bevor wir fortfahren, sollten wir noch die Methode screenDidConnectNotification: verbessern, indem wir die Eigenschaft externalScreen auch setzen, wenn wir darüber benachrichtigt werden, dass ein externer Bildschirm angeschlossen wurde – denken Sie daran, dass das object der UIScreenDidConnectNotification-Notifikation, wie wir bereits erwähnten, das UIScreen-Objekt für das neu angeschlossene Gerät enthält. Wir können uns die externe Anzeige also folgendermaßen beschaffen: ExternalDisplay/ExternalDisplay1/Classes/ExternalDisplayViewController.m
-(void)screenDidConnectNotification:(NSNotification *)notification { statusLabel.text = @"Verbunden"; X externalScreen = (UIScreen *)[notification object]; }
Fügen Sie der Methode screenDidConnectNotification: diesen Code hinzu, indem Sie ausprobieren, wie er sich verhält, wenn das externe Anzeigegerät ein- oder ausgesteckt wird.
200 X Kapitel 10: Externe Anzeigegeräte nutzen
10.2
Einfache Ausgabe auf ein externes Display Da wir jetzt wissen, wann ein externes Display angeschlossen ist, können wir beginnen, es zu nutzen. Als Erstes werden wir eine Fenster/ View-Hierarchie auf dem externen Anzeigegerät erstellen und diese nutzen, um die Farbe des Bildschirms zu bestimmen. Bauen wir dazu zunächst auf dem Hauptbildschirm des iPads einen Farbwähler auf (wir werden uns dabei auf die Wiederverwendung des Farbwählers beschränken, den wir in Kapitel 4, Popover und modale Dialoge, erstellt hatten). Öffnen Sie das Projekt ExternalDisplay/ExternalDisplay2/ ExternalDisplay.xcodeproj. Sie werden sehen, dass wir den Farbwähler aus Kapitel 4, Popover und modale Dialoge, bereits eingefügt haben:
Außerdem haben wir alles so verbunden, dass bei Auswahl einer Farbe der View in der Methode setbackgroundColor: aktualisiert wird: ExternalDisplay/ExternalDisplay2/Classes/ExternalDisplayViewController.m
-(void)setbackgroundColor:(UIColor *)theColor { colorView.backgroundColor = theColor; }
Jetzt benötigen wir einen View auf dem externen Anzeigegerät, damit wir dessen Hintergrundfarbe setzen können. So geht’s: ExternalDisplay/ExternalDisplay2/Classes/ExternalDisplayViewController.m
-(void)setupExternalScreen { if (!externalScreen) { externalScreen = [self scanForExternalScreen]; } if (externalScreen) { statusLabel.text = @"Verbunden"; UIWindow *thisWindow = [[UIWindow alloc] initWithFrame:[externalScreen bounds]];
Einfache Ausgabe auf ein externes Display W 201 [thisWindow setScreen:externalScreen]; externalView = [[UIView alloc] initWithFrame:[thisWindow bounds]]; [externalView setBackgroundColor:[UIColor grayColor]]; [thisWindow addSubview:externalView]; [thisWindow makeKeyAndVisible]; } }
Hier haben wir eine neue Methode erstellt, die sich um die Einzelheiten der Einrichtung einer neuen View-Hierarchie kümmert. Gehen wir diese Methode durch: Zunächst prüfen wir, ob es ein externes Anzeigegerät gibt, und suchen nach einem, wenn das nicht der Fall ist. Finden wir eins, sorgen wir dafür, dass das Statuslabel richtig gesetzt ist, und erstellen dann den View. Können wir auf das Screen-Objekt zugreifen, benötigen wir ein UIWindow, in das wir unseren View platzieren können. Deswegen instantiieren wir als Nächstes ein UIWindow-Objekt und setzen seine screenEigenschaft. Außerdem setzen wir seinen Frame so, dass er den Grenzen des Anzeigegeräts entspricht.2 Dann erstellen wir einen UIView und lassen seine Grenzen auf denen des Fensters basieren. Schließlich setzen wir die Hintergrundfarbe auf Grau, fügen den View als Subview ein und machen das gesamte Fenster sichtbar. Beachten Sie, dass das UIView-Objekt einer Eigenschaft namens externalView zugewiesen wird. Diese Eigenschaft müssen Sie in Ihrer Header-Datei ergänzen.
Mein iPad erkennt den Bildschirm nicht immer wenn ich ihn ein- oder ausstecke Das Ein- und Ausstecken, während die Anwendung läuft, scheint bei Entwickleranwendungen nicht zuverlässig zu funktionieren (auch bei einigen eingebauten Anwendungen wie YouTube ist dies offenbar der Fall). Aber ein Neustart der Anwendung löst das Problem in der Regel.
Geben Sie diesen Code ein. Damit er genutzt wird, müssen Sie in Ihre viewDidLoad:-Methode einen Aufruf der Methode setupExternalScreen: einfügen: [self setupExternalScreen];
2 Wenn Sie eine andere unterstützte Größe für den externen Monitor nutzen wollen, können Sie die UIScreen-Eigenschaft availableModes nutzen, um die vom Monitor unterstützten Modi in Erfahrung zu bringen. Dann können Sie die Eigenschaft currentModes auf den gewünschten Modus setzen.
202 X Kapitel 10: Externe Anzeigegeräte nutzen Außerdem müssen Sie Ihrer screenDidConnectNotification:-Methode Code hinzufügen, damit Fenster und View erstellt werden, wenn das iPad mit einem externen Anzeigegerät verbunden wird (hängen Sie folgenden Code ans Ende Ihrer Methode an): ExternalDisplay/ExternalDisplay2/Classes/ExternalDisplayViewController.m
if (externalScreen == nil) { [self setupExternalScreen]; }
Führen Sie die Anwendung aus. Nachdem Sie diese Änderungen vorgenommen haben, sollten Sie, wenn Sie das iPad mit dem externen Monitor verbinden, einen grauen Bildschirm sehen. Sorgen wir nun dafür, dass die Farbe funktioniert. Nach diesen Vorarbeiten ist das kein großes Problem mehr: Fügen Sie Ihrer setbackgroundColor:-Methode einfach Folgendes hinzu: ExternalDisplay/ExternalDisplay2/Classes/ExternalDisplayViewController.m
-(void)setbackgroundColor:(UIColor *)theColor { colorView.backgroundColor = theColor; X if (externalScreen) { X [externalView setBackgroundColor:theColor]; X } }
Fügen Sie diesen Code ein und testen Sie die Anwendung. Sie sollten Ihren Bildschirm jetzt auf verschiedene Farben setzen können:
Dem externen Bildschirm Views hinzufügen Da wir nun das Fensterobjekt auf dem externen Anzeigegerät haben, können wir beliebige Views erstellen. Denken Sie allerdings daran, dass der
Einfache Ausgabe auf ein externes Display W 203 Benutzer keine Möglichkeit hat, direkt mit diesem Bildschirm zu interagieren. Beispielsweise können wir dem Bildschirm ganz leicht ein hübsches Logo spendieren, indem wir ihm einen UIImageView hinzufügen: ExternalDisplay/ExternalDisplay2/Classes/ExternalDisplayViewController.m
-(void)setupExternalScreen { if (!externalScreen) { externalScreen = [self scanForExternalScreen]; } if (externalScreen) { statusLabel.text = @"Verbunden"; UIWindow *thisWindow = [[UIWindow alloc] initWithFrame:[externalScreen bounds]]; [thisWindow setScreen:externalScreen]; externalView = [[UIView alloc] initWithFrame:[thisWindow bounds]]; [externalView setBackgroundColor:[UIColor grayColor]]; [thisWindow addSubview:externalView]; [thisWindow makeKeyAndVisible]; X X X X X X X X X X X X X
CGRect rect = [externalScreen bounds]; rect.origin.x = (rect.size.width -500) / 2; rect.origin.y = (rect.size.height -300) / 2; rect.size.width = 500; rect.size.height = 300; UIImageView *imageView = [[UIImageView alloc] initWithFrame:rect]; [imageView setImage:[UIImage imageNamed:@"logo.png"]]; imageView.frame = rect; [externalView addSubview:imageView]; } }
Hier beschaffen wir uns einfach die Grenzen des Bildschirms und zentrieren logo.png im UIView. So sieht unsere Version aus:
204 X Kapitel 10: Externe Anzeigegeräte nutzen
10.3
Videoinhalte auf den externen Bildschirm schicken Was wäre wohl besser für die Anzeige auf einem (womöglich großen) externen Bildschirm geeignet als ein Video? Klingt kompliziert? Eigentlich haben wir bereits alles dafür eingerichtet (insbesondere wenn wir etwas Code aus Kapitel 7, Der Movie Player, entlehnen). Öffnen Sie das Projekt ExternalDisplay/ExternalDisplay3/ExternalDisplay.xcodeproj, wir haben dort bereits alles für Sie vorbereitet. Wenn Sie die Nib ExternalDisplayViewController.xib öffnen, sehen Sie, dass wir einen kleinen View und einen kleinen Bildschirm wechseln-Button eingefügt haben:
Bevor wir weiteren Code einfügen, erstellen Sie die Anwendung vorab erst mal. Das Video sollte, wie Sie es in der nächsten Abbildung sehen, auf dem iPad wiedergegeben werden.3 Unsere Aufgabe ist es jetzt, den Wechseln-Button zu aktivieren, damit der Benutzer mit einem Tipp darauf die Anzeige zwischen den beiden Bildschirmen hin- und herschieben kann. Wir haben als Aktion des Buttons bereits die Methode switchMovie festgelegt, die wir jetzt nur noch definieren müssen.
3 Wo Sie das Video erhalten oder wie Sie ein eigenes einsetzen, hatten wir in Kapitel 7, Der Movie Player, beschrieben.
Videoinhalte auf den externen Bildschirm schicken W 205
Überlegen Sie, bevor wir uns an die Arbeit machen, wie wir das anstellen können: Wir müssen ermitteln, wo das Video wiedergegeben wird (auf dem Hauptbildschirm oder auf dem externen Bildschirm), und es dann auf den anderen Bildschirm schieben. Wie wir das machen? Vielleicht erinnern Sie sich aus Kapitel 7, Der Movie Player, dass MPMovieViewController darauf basiert, dass wir ihm einen View geben und er das Video auf diesem View anzeigt. Wir können also prüfen, ob der View des Players aktuell der View auf dem iPad-Bildschirm ist oder der View auf dem externen Bildschirm, und uns dann um die Logistik des Bildschirmwechsels kümmern. Legen wir los: ExternalDisplay/ExternalDisplay3/Classes/ExternalDisplayViewController.m
-(IBAction)switchMovie { if (player.view.superview == viewForMovie) { [player.view removeFromSuperview]; [externalView addSubview:player.view]; } else { [player.view removeFromSuperview]; [viewForMovie addSubview:player.view]; } }
Hier prüfen wir, wie erläutert, ob viewForMovie der Super-View von player.view ist, d.h., ob der View des Players in dem dafür auf dem iPad-Bildschirm bereitgestellten View angezeigt wird. Ist das der Fall, entfernen wir den Player-View aus dem Player und nutzen den externen View. Läuft das Video auf dem externen Bildschirm, entfernen wir diesen View und setzen den View des Players auf viewForMovie auf dem iPad-Bildschirm. Fügen Sie den Code ein, kompilieren Sie ihn und testen Sie die Anwendung.
206 X Kapitel 10: Externe Anzeigegeräte nutzen Für einen ersten Versuch war das gar nicht übel. Das Video wird wie erwartet zwischen den Bildschirmen hin- und hergeschoben. Aber ein paar Feinarbeiten sind noch erforderlich – wenn Sie sehen, was wir sehen, dürfte Ihnen aufgefallen sein, dass die Größe des Videos nicht an den externen Bildschirm angepasst wird. Versetzen wir die externe Anzeige also in den Vollbildmodus, wenn gewechselt wird: ExternalDisplay/ExternalDisplay3/Classes/ExternalDisplayViewController.m
-(IBAction)switchMovie { if (player.view.superview == viewForMovie) { [player.view removeFromSuperview]; [externalView addSubview:player.view]; X [player setFullscreen:YES animated:NO]; } else { [player.view removeFromSuperview]; [viewForMovie addSubview:player.view]; X [player setFullscreen:NO animated:NO]; } }
Kompilieren Sie den Code und testen Sie ihn – und ist das nicht genau das, was wir haben wollten? Wenn wir auf den externen Monitor wechseln, sehen wir das Video im Vollbildmodus. Hübsch!
Zusammenfassung W 207
10.4
Zusammenfassung In diesem Kapitel haben Sie gesehen, dass Sie etwas Arbeit leisten müssen, um den zweiten Bildschirm zu verwalten, aber darüber hinaus gibt es bei der Befüllung des Bildschirms keine Unterschiede: Sie erstellen einfach Views, genau wie auf dem Hauptbildschirm. Natürlich dürfen wir nicht vergessen, dass der zweite Bildschirm kein MultiTouch-Gerät ist (zumindest noch nicht) und wir den zweiten Bildschirm häufig als andere Art von Anzeigegerät behandeln als unseren Hauptbildschirm. Obwohl wir den Hauptbildschirm nicht auf einen externen Monitor werden spiegeln können, bis Apple uns die Möglichkeit dazu gibt, ist das ohnehin nicht immer das, was wir tun wollen. Aber für den Augenblick reichen die uns zur Verfügung stehenden Werkzeuge, um einige interessante Anwendungen zu erstellen, die einen externen Bildschirm nutzen, so einer vorhanden ist.
Kapitel 11
Geräte verbinden Das iPad hat einen so großen Bildschirm, dass man darauf problemlos die verschiedensten Brettspiele spielen kann, z.B. Schach, Go, Backgammon und so weiter. Aber man könnte kein Spiel spielen, bei dem es wichtig ist, dass der eine Spieler nicht die Karten oder Spielsteine des anderen einsehen kann. Das verhindert Spiele wie Mastermind, Scrabble, Schiffe versenken und die meisten Kartenspiele.1 Oder etwa nicht? Was wäre, wenn wir nicht nur ein iPad für das Spiel nutzen würden? Was wäre, wenn wir mehrere iPads oder ein iPad als gemeinsames Brett und iPhones und iPod touchs für die Dinge nutzen würden, die die anderen Spieler nicht sehen sollen? 2 Auch wenn es recht idiotisch klingt, Geräte im Wert von Tausenden von Euro zu nutzen, um damit ein ein Euro-Kartenspiel zu spielen, sollten Sie sich darüber Gedanken machen, was sich ändert, wenn mehrere Personen kompatible Geräte haben. Welche neuartigen Erlebnisse könnten Sie sich für mehrere verbundene Geräte ausdenken, die bei einem einzigen Gerät wenig Sinn ergeben? Wenn Sie eine Anwendung aufbauen, sollten Sie immer überlegen, auf welche Weise man sie mächtiger machen kann, wenn sie in der Nähe laufende Instanzen der gleichen App finden könnte. In diesem Kapitel werden wir uns zwei einfache Beispiele ansehen, die unterschiedliche Techniken demonstrieren, die Sie nutzen, um eine Kommunikation zwischen Ihrem iPad und einem anderen Gerät zu ermöglichen. Im ersten Beispiel erstellen wir ein Client/Server-Netz1 Die Rechte an Mastermind hält Invicta Plastics, die Rechte an Scrabble Hasbro, und die Rechte an Schiffe versenken hält die Milton Bradley Company. 2 Im App Store gibt es bereits Beispiele für Multi-Geräte-Apps für verschiedene Spiele und ernsthaftere Anwendungen.
210 X Kapitel 11: Geräte verbinden werk, um ein klassisches Problem, das sogenannte Monty Hall-Problem, zu lösen. Im zweiten Beispiel implementieren wir ein einfaches Peer-to-Peer-Chatsystem, das ein anderes Mittel zur Entdeckung und Verbindung mit Peers im Netzwerk vorführt. Beide Beispiele nutzen Apple’s Game Kit-Framework sowie Bluetooth, um Geräte zu verbinden, die sich im Umkreis von einigen Metern aufhalten.
11.1
Das Monty Hall-Problem Als erstes Beispiel zeigen wir Ihnen ein Fragespiel, das auf der alten amerikanischen Fernsehshow Let’s Make a Deal basiert. Namensgeber für das entsprechende Problem ist der langjährige Showmaster der Show, Monty Hall. Das Problem sieht folgendermaßen aus. Es gibt drei Türen. Hinter einer ist irgendein wertvoller Preis, hinter den beiden anderen nichts Wertvolles. Der Spieler wählt eine Tür. Dann öffnet der Showmaster eine der Türen, die der Spieler nicht gewählt hat, und zeigt, dass nichts Wertvolles dahinter ist. Der Showmaster gibt dem Spieler nun die Möglichkeit, bei seiner alten Entscheidung zu bleiben oder die Tür zu wechseln. Es geht also um die Entscheidung, ob der Teilnehmer wechseln soll oder ob es keinen Unterschied macht, ob er wechselt oder nicht.3 Wir werden zwei unterschiedliche Anwendungen erstellen. Eine präsentiert das Spiel aus Sicht des Spielers, die andere aus Sicht des Showmasters. Wir hätten auch eine einzige Anwendung erzeugen können, die entweder als Showmaster- oder als Teilnehmer-Schnittstelle fungieren könnte. Aber in diesem Fall sind die Rollen so unterschiedlich, dass es sinnvoll ist, dass die eine Anwendung als Server, die andere als Client dient. Es ergäbe keinen Sinn, würden sich zwei Teilnehmer oder zwei Showmaster zusammentun. Wenn Sie hingegen ein Kartenspiel schreiben, könnte es sinnvoller sein, dass die Spieler Partner (Peers) sind. Sie finden die beiden Anwendungen im Download-Code. Die eine heißt Contestant. Es ist eine View-basierte iPad-Anwendung. Die andere heißt MontyHall. Wir haben uns entschlossen, diese als View-basierte iPhone-Anwendung auszuführen, weil nicht viele Entwickler zwei iPads besitzen, sondern eher ein iPad und ein iPhone oder einen iPod touch.4 3 Die Lösung ist, dass der Teilnehmer immer wechseln sollte. Wenn Sie die Rolle des Showmasters in diesem Spiel für zwei Personen übernommen haben, werden Sie verstehen, warum. 4 Wir hätten ebenfalls eine universelle Anwendung für beide Geräte erstellen können. Aber wir wollten, dass sich der Code ganz auf das konzentrieren kann, was hier Gegenstand ist. Natürlich können Sie auch mehrere iPads, iPhones oder eine Mischung aus beiden verbinden.
Den Server starten und bekannt machen W 211 Für die Kommunikation zwischen den Geräten nutzen wir Game Kit, Sie müssen also beide Anwendungen auf den entsprechenden Geräten installieren. Außerdem werden Sie mit den Ergebnissen – dieser Ratschlag entstammt unser eigenen Erfahrung – erheblich glücklicher sein, wenn Sie sichern, dass auf beiden Geräten Bluetooth aktiviert ist. Das müssen Sie über Einstellungen machen.
11.2
Den Server starten und bekannt machen Die Contestant-App fungiert als Server. Wenn der Controller den View lädt, erzeugt sie ein GKSession-Objekt, setzt den View-Controller als Session-Delegate und verkündet mit der Methode initWithSessionID: displayName:sessionMode:, dass die Session jetzt verfügbar ist.5 Die Methode initWithSessionID:displayName:sessionMode: erwartet drei Parameter. Der erste ist ein Dienstname im Bonjour-Stil. Sie sollten Ihren Namen bei der DNS-SD-Registrierung (http://www.dnssd.org/ServiceTypes.html) registrieren. Wir nutzen example als Dienstname, weil dieser speziell für die Verwendung in Büchern und ihren Beispielen reserviert ist. Mit dem zweiten Parameter übergeben Sie einen gewöhnlichen Namen, über den andere Sie erkennen können. Hier geben wir Contestant vor, aber meistens lassen Sie dazu den Benutzer einen Namen eingeben. Über den letzten Parameter geben Sie den Modus der Session an, die Sie starten. Sie können Server sein und die Session bekannt machen, damit andere sie finden, aber auch ein Client, der bekannt gemachte Sessions sucht, oder ein Peer, der gleichermaßen Sessions bekannt macht und nach bekannt gemachten Sessions sucht.6 Wir werden die Session in der Methode launchServerSession initialisieren, die wir in der Methode viewDidLoad aufrufen.
5
Sie müssen natürlich auch daran denken, in ContestantViewController.h das
GKSessionDelegate-Protokoll zu deklarieren.
6 Die Terminologie ist etwas verwirrend, weil Sie, nachdem einmal eine Verbindung mit einer Session erstellt wurde, alle Geräte, mit denen Sie verbunden sind, als Peer bezeichnen, unabhängig davon, wie diese sich mit der Session verbinden.
212 X Kapitel 11: Geräte verbinden Devices/Contestant1/Classes/ContestantViewController.m
-(void)launchServerSession { self.session = [[GKSession alloc] initWithSessionID:@"example" displayName:@"Contestant" sessionMode:GKSessionModeServer]; self.session.delegate = self; self.session.available = YES; }
Wenn Sie die neue Session bekannt gemacht haben, können Sie nur noch warten. Anders gesagt, Sie implementieren die entsprechenden Delegate-Methoden, und diese werden aufgerufen, wenn es eine Änderung gibt, auf die der Host reagieren muss. Der Host muss reagieren, wenn er eine Verbindungsanfrage erhält und wenn er erkennt, dass sich ein anderes Gerät erfolgreich verbunden hat oder eine Verbindung beendet wurde. Zunächst werden wir uns darum kümmern, dass reagiert wird, wenn ein Client eine Verbindung anfordert, indem wir folgendermaßen die Methode session:didRecieveConnectionRequestFromPeer: implementieren: Devices/Contestant1/Classes/ContestantViewController.m
-(void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID { self.statusField.text = [NSString stringWithFormat:@"%@ möchte unser Showmaster sein.", [self.session displayNameForPeer:peerID]]; NSError *error1; [self.session acceptConnectionFromPeer:peerID error:&error1]; }
In einer komplexeren Anwendung könnten Sie einen modalen View mit einer Nachricht anzeigen, in der der Benutzer festlegen kann, ob er die Anfrage akzeptieren will oder nicht. Wir werden hier jede Anfrage akzeptieren, und zwar in der Nachricht an das GKSession-Objekt, die als Parameter für acceptConnectionFromPeer: übergeben wurde. Wenn Sie eine Anwendung schreiben, in der Geräte irgendwie über ein Netzwerk kommunizieren, müssen Sie berücksichtigen, dass Geräte auftauchen und verschwinden. Diese Änderungen überwachen wir, indem wir die Methode session:peer:didChangeState: implementieren, damit wir benachrichtigt werden, wenn die Verbindung zu einem Peer hergestellt bzw. unterbrochen wird.
Den Client starten und verbinden W 213 Devices/Contestant1/Classes/ContestantViewController.m
- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state { switch (state) { case GKPeerStateConnected: [self connectToPeer:peerID]; break; case GKPeerStateDisconnected: self.statusField.text = @"Showmaster hat Verbindung beendet."; //Hier kommt der eigentliche Handling-Code hin. break; default: break; } }
Sobald wir mit einem Client verbunden werden, rufen wir eine Methode namens connectToPeer: auf, damit wir das, was wir tun, wenn wir über eine Verbindung benachrichtigt werden, in einer eigenen Methode isolieren können. In connectToPeer: zeigen wir eine Nachricht an, die sagt, dass wir verbunden sind, und setzen den Status der Session auf nicht mehr verfügbar, damit sich keine weiteren Clients verbinden können. Außerdem werden wir die Methode setDataReceiveHandler: nutzen, um der Session zu sagen, welches Objekt für die Verarbeitung der Daten zuständig ist, die an den Peer geschickt werden. Hier veranlassen wir den View-Controller, sich um alle Daten zu kümmern, die wir erhalten. Devices/Contestant1/Classes/ContestantViewController.m
-(void)connectToPeer:(NSString *) peerID{ self.statusField.text = [NSString stringWithFormat: @"%@ ist unser Showmaster.", [self.session displayNameForPeer:peerID]]; self.session.available = NO; [self.session setDataReceiveHandler:self withContext:nil]; }
Bevor wir weitere Teile der Servers implementieren, sollten wir uns ansehen, was wir tun müssen, um den Client zu starten und den Server finden zu lassen.
11.3
Den Client starten und verbinden Der Code, der genutzt wird, um den Client zu starten, ist fast identisch mit dem, den wir verwendet haben, um den Server zu starten. Aber diesmal ist der Modus auf GKSessionModeClient gesetzt:
214 X Kapitel 11: Geräte verbinden Devices/MontyHall1/Classes/MontyHallViewController.m
-(void) launchClientSession { self.session = [[GKSession alloc] initWithSessionID:@"example" displayName:@"Monty" X sessionMode:GKSessionModeClient]; self.session.delegate = self; self.session.available = YES; }
Wieder implementieren Sie die Delegate-Methode session:peer:didChangeState:, um auf Zustandsänderungen zu reagieren, die von den Peers festgestellt werden. Hier werden wir darauf reagieren, dass ein verfügbarer Peer gefunden, eine Verbindung mit dem Peer hergestellt und die Verbindung zu dem Peer getrennt wird. Devices/MontyHall1/Classes/MontyHallViewController.m
-(void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state { switch (state) { case GKPeerStateAvailable: [self connectToAvailablePeer:peerID]; break; case GKPeerStateConnected: [self beginSession]; break; case GKPeerStateDisconnected: //Trennung der Verbindung verarbeiten break; default: break; } }
Wenn wir einen verfügbaren Peer entdecken, versuchen wir, eine Verbindung zu ihm herzustellen. Wir erstellen die Methode connectToAvailablePeer:, die wir aufrufen, wenn der Verbindungszustand GKPeerStateAvailable ist. In dieser Methode aktualisieren wir die Nachricht, die wir anzeigen, und rufen die Methode connectToPeer:withTimeout: auf, um zu versuchen, eine Verbindung mit dem entdeckten Gerät herzustellen. Devices/MontyHall1/Classes/MontyHallViewController.m
-(void) connectToAvailablePeer:(NSString *) peerID { self.statusField.text = [NSString stringWithFormat: @"Verbinde mit %@.", [self.session displayNameForPeer:peerID]]; [self.session connectToPeer:peerID withTimeout:3000]; }
Die Spiellogik ergänzen W 215 Wenn das Gerät, das unserer Anwendung als Peer bekannt ist, berichtet, dass sich sein Zustand in GKPeerStateConnected geändert hat, rufen wir unsere beginSession-Methode auf. In dieser senden wir dem Benutzer eine Nachricht, die ihm sagt, dass er mit dem Spielen beginnen kann, und markieren die Session als nicht mehr verfügbar. Devices/MontyHall1/Classes/MontyHallViewController.m
-(void) beginSession { self.statusField.text = @"Wählen Sie eine Tür zur Unterbringung des Preises."; self.session.available = NO; }
Wenn Sie die MontyHall- und die Contestant-Anwendung auf unterschiedlichen iOS-Geräten ausführen, auf denen Bluetooth aktiviert ist, beobachten Sie ruhig mal, wie sich die Geräte entdecken und eine Session beginnen.
11.4
Die Spiellogik ergänzen Wir werden unseren Showmaster so implementieren, dass er einen Button für jede der Türen hat. Richten Sie Outlets namens door1, door2 und door3 für die Türen ein und deklarieren Sie eine Aktion namens selectDoor:, die von allen Buttons aufgerufen wird, wenn sie gedrückt werden. Außerdem fügen wir eine Eigenschaft des Typs NSArray namens doors ein, mit der wir die Türen festhalten. Wenn wir die Session beginnen, erstellen wir ein Array namens doors, das aus drei Buttons besteht. Wir machen die Buttons sichtbar und setzen den View-Controller für den Empfang aller Daten, die vom Teilnehmer gesendet werden. Devices/MontyHall2/Classes/MontyHallViewController.m
X X X X X X X
-(void) beginSession { self.statusField.text = @"Wählen Sie eine Tür zur Unterbringung des Preises."; self.session.available = NO; self.doors = [NSArray arrayWithObjects:self.door1, self.door2, self.door3, nil]; [self setAllDoorsToHidden:NO]; [self.session setDataReceiveHandler:self withContext:nil]; [door1 release]; [door2 release]; [door3 release]; }
216 X Kapitel 11: Geräte verbinden Der View-Controller wartet darauf, dass der Benutzer eine Tür wählt. Während des Spielablaufs gibt es zwei Gelegenheiten, zu denen eine Tür gewählt werden kann. Bei diesem ersten Mal wählen wir die Tür, hinter der der Preis verborgen wird; isPrizeInPlace hat also den Wert NO. Wenn Sie sich unsere Aktion ansehen, werden Sie feststellen, dass wir beim ersten Durchlauf die Methode placePrizeBehindDoor: aufrufen. Devices/MontyHall2/Classes/MontyHallViewController.m
-(IBAction) selectDoor:(id)sender { if (isPrizeInPlace) { [self revealDoor:sender]; } else { [self placePrizeBehindDoor:sender]; } }
Die Methode placePrizeBehindDoor: deaktiviert die Buttons, damit der Showmaster keine weiteren Änderungen vornehmen kann, zeigt eine Nachricht an und ändert die Farbe der ausgewählten Tür in Grün, die der beiden anderen in Rot. Sie können auch sehen, dass wir die Nummer der Tür speichern, die gewählt wurde, und den Schalter isPrizeInPlace umlegen, damit bei der nächsten Aktivierung eines Buttons die Methode revealDoor: aufgerufen wird. Devices/MontyHall2/Classes/MontyHallViewController.m
-(void) placePrizeBehindDoor:(id)sender { [self setAllDoorsToEnabled:NO]; self.statusField.text = @"Warten Sie, während der Spieler seine Wahl trifft."; prizeDoor = ((UIButton *)sender).tag; [self setAllDoorsRedExcept:(UIButton *)sender]; [self sendMessage:@"hideThePrize:" forDoor:prizeDoor]; isPrizeInPlace = YES; }
Wurde die dritte Tür gewählt, sieht unsere Schnittstelle ungefähr so aus:
Daten an ein anderes Gerät senden W 217
In der Methode placePrizeBehindDoor: wird eine weitere wichtige Aktion ausgeführt. Die Nachricht sendMessage:forDoor: wird versandt. Das leitet das Versenden der Informationen zu der gewählten Tür an das verbundene Gerät ein.
11.5
Daten an ein anderes Gerät senden Alles, was wir von einem Gerät an ein anderes senden, muss als NSData verpackt werden. In unserer sendMessage:forDoor:-Methode bereiten wir die zu sendenden Daten vor, indem wir ein Schlüsselarchiv erstellen, dessen Schlüssel dem Namen der Methode entspricht, die am anderen Ende aufgerufen wird, und dessen Wert die Türnummer ist. Dann senden wir dem Teilnehmer diese Daten mit folgendem Aufruf: Devices/MontyHall2/Classes/MontyHallViewController.m
[self.session sendDataToAllPeers:data withDataMode:GKSendDataReliable error:&error2];
Wir könnten die Daten an alle Peers oder einen bestimmten Peer senden. Wir senden hier die Daten an alle Peers – da wir nur einen einzigen Peer haben, ist es sinnlos, die Adressaten einzuschränken. Wir können angeben, ob die Daten zuverlässig versendet werden müssen. Wir haben GKSendDataReliable gewählt, die Daten werden also so lange
218 X Kapitel 11: Geräte verbinden verschickt, bis die Operation erfolgreich ist oder ein Verbindungs-Timeout eintritt. Kombinieren wir das mit der Verpackung der Daten und der Speicherverwaltung, sieht unsere sendMessage:forDoor:-Methode so aus: Devices/MontyHall2/Classes/MontyHallViewController.m
-(void) sendMessage:(NSString *)message forDoor:(NSUInteger) doorNumber { NSMutableData *data = [[NSMutableData alloc] init]; NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data]; [archiver encodeInt:doorNumber forKey:message]; [archiver finishEncoding]; NSError *error2; [self.session sendDataToAllPeers:data withDataMode:GKSendDataReliable error:&error2]; [archiver release]; [data release]; }
So werden die Daten an alle Peers gesendet. Schauen wir als Nächstes, was der Teilnehmer tut, wenn er die Nachricht empfängt.
11.6
Von einem anderen Gerät gesendete Daten empfangen Sie erinnern sich sicher daran, dass wir das ContestantViewController -Objekt mit folgendem Aufruf für den Empfang von Daten registriert haben, die in der Session versendet werden: Devices/Contestant2/Classes/ContestantViewController.m
[self.session setDataReceiveHandler:self withContext:nil];
Wenn unsere Session Daten empfängt, ruft das System die DelegateNachricht namens receiveData:fromPeer:inSession:context: auf. Wir werden die Daten auspacken und schauen, welcher Schlüssel für die Daten gesendet wurde. Wir rufen die Methode mit diesem Namen auf und senden die Türnummer als Argument. Devices/Contestant2/Classes/ContestantViewController.m
-(void) receiveData: (NSData*) data fromPeer: (NSString*) peerID inSession: (GKSession*) session context: (void*) context { NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data]; if ([unarchiver containsValueForKey:@"hideThePrize:"]) { [self hideThePrize: [unarchiver decodeIntForKey:@"hideThePrize:"]];
Von einem anderen Gerät gesendete Daten empfangen W 219 } else if ([unarchiver containsValueForKey:@"revealAnEmptyDoor:" ]){ [self revealAnEmptyDoor: [unarchiver decodeIntForKey:@"revealAnEmptyDoor:" ]]; } }
In diesem Fall rufen wir die Methode hideThePrize: auf. Es ist nicht sonderlich interessant, was die Methode macht, aber ab einem gewissen Punkt kann der Contestant-Nutzer eine Tür wählen, die verpackt und zurück an die MontyHall-Anwendung gesendet wird. Um ein weiteres Verfahren zu illustrieren, nutzen wir in der MontyHallApp eine etwas andere Strategie, um die Daten zu empfangen. Diesmal werden wir einige der Wiederholungen aus der Methode receiveData:fromPeer:inSession:context: entfernen: Devices/MontyHall2/Classes/MontyHallViewController.m
-(void) receiveData: (NSData*) data fromPeer: (NSString*) peerID inSession: (GKSession*) session context: (void*) context { NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data]; [self invokeMethodForKey:@"contestantDidChooseDoor:" ifFoundIn:unarchiver]; [self invokeMethodForKey:@"finalDoorSelected:" ifFoundIn:unarchiver]; }
Die Methode invokeMethodForKey: enthält alle Wiederholungen, die wir aus der Delegate-Methode entfernt haben. Das beinhaltet beispielsweise die Prüfung, ob der unarchiver einen Schlüssel mit einem bestimmten Namen enthält, bevor die entsprechende Methode aufgerufen und der dem Schlüssel entsprechende Wert übergeben wird. Devices/MontyHall2/Classes/MontyHallViewController.m
-(void) invokeMethodForKey:(NSString *) keyName ifFoundIn:(NSKeyedUnarchiver *)unarchiver { if ([unarchiver containsValueForKey:keyName]) { [self performSelector:NSSelectorFromString(keyName) withObject:[NSNumber numberWithInt:[unarchiver decodeIntForKey:keyName]]]; } }
Das sind zwei Varianten, Daten zu behandeln, die einen Methodennamen und das Argument für die Methode angeben. Eventuell müssen Sie ganz andere Arten von Daten zwischen Geräten austauschen, die Sie auf unterschiedliche Weise formatieren wollen. Weil Sie beide Enden der Konversation steuern, können Sie beliebige Formate erstellen, wenn Sie senden wollen, und später entsprechend verarbeiten,
220 X Kapitel 11: Geräte verbinden wenn Sie sie empfangen. Ein weiteres mögliches Protokoll werden wir uns später in diesem Kapitel ansehen.
11.7
Aufräumen Hat der Contestant-Benutzer seine endgültige Wahl getroffen, sieht er, ob er gewonnen hat, und sendet seine endgültige Wahl an die MontyHall-Anwendung, damit der Showmaster sehen kann, ob der Spieler gewonnen hat. An diesem Punkt ist das Spiel vorüber. Weder der MontyHall- noch der Contestant-Benutzer hat weitere Verwendung für die Verbindung zwischen den beiden Geräten. Deswegen rufen wir im MontyHallViewController die Methode disconnectFromAllPeers auf. Devices/MontyHall2/Classes/MontyHallViewController.m
-(void) finalDoorSelected:(NSNumber *)doorNumberObject { NSUInteger doorNumber = [doorNumberObject intValue]; [self setAllDoorsToHidden:YES]; [self setDoorNumber:doorNumber toHidden:NO]; if (doorNumber == prizeDoor) { self.statusField.text = @"Der Teilnehmer gewinnt."; } else { self.statusField.text = @"Der Teilnehmer verliert."; } X [self.session disconnectFromAllPeers]; }
11.8
Peers bekannt machen In diesem zweiten Beispiel erstellen wir eine einfache Chatanwendung und nutzen die Klasse GKPeerPickerController, um die Verfügbarkeit der Anwendung bekannt zu machen. Sie können im Verzeichnis PeerChat1 sehen, dass wir eine View-basierte iPhone-Anwendung mit einem einzigen UITextField erstellt haben.7 Der PeerChatViewController fungiert als UITextFieldDelegate des Textfelds. Wenn der Benutzer seinen Namen eingibt, erstellt und startet die Anwendung eine Instanz eines GKPeerPickerController.
7 Das könnte auch eine iPad-Anwendung sein. Wir hatten ja bereits gesagt, dass wir eine iPhone-App erstellt haben, weil Sie zwei Geräte benötigen, um diese Anwendung zu testen.
Peers bekannt machen W 221
Wir werden den Chat über Bluetooth einrichten, indem wir die Typmaske des Peer-Picker-Controllers auf GKPeerPickerConnectionTypeNearBy setzen. Wenn sich die Geräte einander nähern, beispielsweise auf zehn Meter, ist Bluetooth eine gute Wahl. Sie können auch WLAN nutzen, aber dann kann das Problem auftauchen, dass die Geräte mit unterschiedlichen Netzwerken verbunden sind und einander nicht sehen. Außerdem müssen Sie bei WLAN eine eigene Schnittstelle erstellen, um eine Internetverbindung einzurichten. Folgendermaßen erstellen wir das GKPeerPickerController -Objekt und setzen den PeerChatViewController als sein Delegate. Wir konfigurieren die Peer-Picker-Verbindung über Bluetooth und zeigen dann den Peer-Picker an: Devices/PeerChat1/Classes/PeerChatViewController.m
-(void)createAndStartPeerPicker { self.peerPC =[[GKPeerPickerController alloc] init]; self.peerPC.delegate = self; self.peerPC.connectionTypesMask = GKPeerPickerConnectionTypeNearby; [self.peerPC show]; }
Als Nächstes werden wir den Peer-Picker-Controller nutzen, um Sessions zu erstellen und zu verbinden.
222 X Kapitel 11: Geräte verbinden
11.9
Peers verbinden Wir müssen für den Peer-Picker-Controller zwei Delegate-Methoden implementieren. Die Methode peerPickerController:sessionForConnectionType: liefert eine Session, die wir im Getter verzögert initialisieren.8 Der Code sollte bereits vertraut wirken. Allerdings wird der SessionModus auf GKSessionModePeer statt auf Client oder Server gesetzt, wie bei den Sessions, die wir zuvor in diesem Kapitel initialisiert haben: Devices/PeerChat2/Classes/PeerChatViewController.m
-(GKSession *) session { if (!session) { self.session = [[GKSession alloc] initWithSessionID:@"example" displayName:self.userName sessionMode:GKSessionModePeer]; self.session.delegate = self; [self.session setDataReceiveHandler:self withContext:nil]; } return session; }
In diesem Fall sehen wir andere PeerChat-Instanzen, die chatten wollen.
8 Mit verzögerter Initialisierung meinen wir, dass wir das Objekt erst erstellen, wenn es benötigt wird.
Chatten W 223 Wählt eine Person eine andere aus, hat das Gerät der anderen Person eine Möglichkeit, darauf zu reagieren.
All dies geschah, weil wir in der vorangegangenen Delegate-Methode eine Session zurückgeliefert haben. Ist eine Verbindung hergestellt, müssen wir den Peer-Picker-Controller freigeben und sichern, dass die Session korrekt gesetzt ist. Das tun wir in der folgenden DelegateMethode: Devices/PeerChat2/Classes/PeerChatViewController.m
-(void)peerPickerController:(GKPeerPickerController *)picker idConnectPeer:(NSString *)peerID toSession:(GKSession *)activeSession { self.session = activeSession; [picker dismiss]; self.peerPC = nil; }
Jetzt sind die beiden Peers verbunden und zur Kommunikation bereit.
11.10
Chatten Sie werden sehen, dass wir dem PeerChat3-Ordner einen UITextView und eine entsprechende Eigenschaft namens chatView hinzugefügt haben. Gibt einer der Benutzer eine Nachricht in das Textfeld ein, zei-
224 X Kapitel 11: Geräte verbinden gen wir diese Nachricht oben im Text-View an und senden sie an die verbundenen Peers. Wie zuvor werden wir die Methode sendDataToAllPeers:withDataMode:error: nutzen, um die Daten an die anderen Geräte zu senden. Weil wir einen String senden, können wir die Daten vor Ort kodieren. Devices/PeerChat3/Classes/PeerChatViewController.m
-(void) sendAllPeersMessage:(NSString *) message { [self.session sendDataToAllPeers: [message dataUsingEncoding:NSUTF8StringEncoding] withDataMode:GKSendDataReliable error:nil]; }
Und auf ähnliche Weise verarbeiten wir die Daten, die wir von den anderen Peers erhalten. Devices/PeerChat3/Classes/PeerChatViewController.m
-(void) receiveData: (NSData*) data fromPeer: (NSString*) peerID inSession: (GKSession*) session context: (void*) context { [self addMessage:[[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease] forParticipant:[self.session displayNameForPeer:peerID]]; }
Schließlich müssen wir den anderen Peers Bescheid geben, wenn einer der Peers die Verbindung löst. Das werden wir hier implementieren: Devices/PeerChat3/Classes/PeerChatViewController.m
-(void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state { switch (state) { case GKPeerStateDisconnected: [self addMessage:@"*** hat die Verbindung getrennt" forParticipant:[self.session displayNameForPeer:peerID]]; //Die Trennung der Verbindung verarbeiten break; default: break; } }
Das ist eine ziemlich primitive Chatanwendung, dennoch ermöglicht sie uns, einen Blick auf die Einrichtung und Verwendung von Peer-Geräten zu werfen.
Zusammenfassung W 225
11.11
Zusammenfassung Dieses Kapitel war mit vielen coolen Techniken vollgepfropft. Aber vielleicht fragen Sie sich, was es Ihnen nützt? Das ist eine ziemlich wichtige Frage, die Sie sich stellen sollten, gerade wenn Sie eine Anwendung bauen, bei der der Nutzen dieser Techniken nicht unmittelbar offensichtlich ist. Einst war iTunes bloß eine Anwendung, mit der Sie Musik erwerben, brennen und anhören konnten. Dann wurde diese Musikanwendung Bonjour-fähig, und plötzlich hatte iTunes die Möglichkeit, Musik zu teilen und die Musik anderer zu entdecken und anzuhören. In diesem Kapitel haben wir Ihnen zwei unterschiedliche Strategien zur Entdeckung in der Nähe befindlicher Geräte vorgestellt, auf denen die gleiche Anwendung ausgeführt wird wie auf Ihrem. Sie haben gelernt, wie man Daten zwischen diesen anderen Geräten und Ihrem Gerät austauscht. Diese Techniken sind nicht einfach, aber auch nicht so kompliziert. Sie sollten immer erwägen, ob es Ihre Anwendung bereichert, wenn sie eine oder mehrere in der Nähe auf einem anderen iPhone oder iPad laufenden Instanzen der gleichen App aufspüren könnte.
Kapitel 12
Mit Dokumenten arbeiten Die Art und Weise, auf Apples mobilen Geräten mit Dokumenten zu arbeiten, unterlag in der Vergangenheit einem kontinuierlichen Fortschritt. Der iPod war für den Konsum von Medien und Dokumenten gedacht, die an anderer Stelle produziert wurden. Auf dem iPod konnten Sie Musik hören, Videos betrachten und Ihren Kalender sowie Ihre Adressen einsehen. Änderungen an Ihren Terminen oder Kontaktdaten konnten Sie nicht vornehmen. Das iPhone und der iPod touch haben das geändert. Auf ihnen können Sie Textmeldungen und E-Mails schreiben und neue Inhalte erstellen. Obwohl das Erzeugen von neuem Content auf diesen Geräten möglich ist, ist ihr primärer Verwendungszweck immer noch der Konsum von Inhalten, die woanders hergestellt wurden. Das iPad erweitert dieses Spektrum. Es ist so groß, dass man darauf Dokumente verfassen kann. Apple machte diesen Unterschied dadurch deutlich, dass iPad-Versionen von Pages, Numbers und Keynote an dem Tag veröffentlicht wurden, als das iPad auf den Markt kam. Anhand dieser iWork-Anwendungen können Sie sehen, wie sich Apple Ihre Arbeit mit Dokumenten auf Ihrem iPad vorstellt. In diesem Kapitel werden wir uns mehrere Aspekte der Arbeit mit Dokumenten ansehen. Sie werden erfahren, wie Sie die Dokumente aus dem Documents-Verzeichnis Ihrer Anwendung mit iTunes austauschen. Sie werden Dokumente in diesem Verzeichnis öffnen und speichern. Sie werden Ihre Anwendung registrieren, um bekannt zu machen, dass sie
228 X Kapitel 12: Mit Dokumenten arbeiten einen bestimmten Dokumenttyp öffnen kann, und werden die Klasse UIDocumentInteractionController und ihr Delegate nutzen, um andere Anwendungen Dokumente öffnen zu lassen, die Ihre Anwendung nicht öffnen kann. Zum Abschluss werden Sie erfahren, wie man eine Dokumentvorschau anbietet. Ob es nun ein Leid oder ein Segen ist: Das iPad hat keinen Finder. Ihre Anwender sollen nicht in Verzeichnisstrukturen denken müssen. Sie sollen sich keine Gedanken darüber machen müssen, wo ein Dokument gespeichert ist. Sie sollten mit den Dokumenten nur arbeiten, und unser Ziel muss es sein, es ihnen so leicht wie möglich zu machen.
12.1
Dokumente mit iTunes übertragen In Abschnitt 6.6, Unsere Zeichnung als PDF speichern, auf Seite 129 haben wir unser Warnsymbol in einer PDF-Datei gespeichert und diese im Documents-Verzeichnis abgelegt. Jede Anwendung hat ein eigenes DocumentsVerzeichnis, und bis iOS 3.2 war der Inhalt dieses Verzeichnisses für den Benutzer unsichtbar. Es ist nur eine einfache Ergänzung in der PList der Anwendung erforderlich, damit Sie Dateien aus diesem Verzeichnis mit einem Rechner, auf dem iTunes läuft, austauschen können. Öffnen Sie die Version des Bézier-Projekts, die wir zum Schreiben des PDFs genutzt haben. Diesen Code sollten Sie in den Downloads im Verzeichnis /code/Drawing/Bezier12/ finden. Öffnen Sie Bezier-Info.plist mit einem Texteditor und fügen Sie dem Dictionary folgendes Schlüssel/Wert-Paar hinzu: UIFileSharingEnabled <true/>
Falls Sie es vorziehen, können Sie auch doppelt auf die PList klicken und den PList-Editor nutzen, um den Eintrag einzufügen. Die entsprechende Phrase im Drop-down ist „Application supports iTunes file sharing“.
Sollte sich die Bézier-App bereits auf dem Gerät befinden, löschen Sie sie, damit die neuen Einstellungen übernommen werden, wenn wir mit iTunes synchronisieren.
Dokumente dauerhaft speichern Erstellen Sie die App und führen Sie sie aus. Setzen Sie dabei das Gerät als Ziel. Haben Sie die Anwendung auf dem Gerät, beenden Sie sie und synchronisieren Ihr iPad mit iTunes. Wenn Ihr Gerät ausgewählt ist, sollten Sie unten auf der Apps-Seite etwas sehen, wie in Abbildung 12.1 gezeigt. Mehr müssen Sie nicht tun, um den Dokumentaustausch zwischen Ihrem Computer und Ihrem iPad zu aktivieren.1 Die von Ihnen hinzugefügten Dokumente werden auf der obersten Ebene des Documents-Verzeichnisses der Anwendung gespeichert. Speichern Sie Warning.pdf auf Ihrem Computer, indem Sie die Datei aus iTunes herausziehen und öffnen. Sie werden das Warnsymbol sehen, das wir auf dem iPad gezeichnet haben. Sie können dem iPad Dateien hinzufügen, die sich auf Ihrem Computer befinden, aber das bringt jetzt noch nichts. Die Bézier-App hat keine Ahnung, wie man Dateien öffnet. Verändern wir dazu die Feelings-App aus Kapitel 5, Angepasste Tastaturen.
Abbildung 12.1: Die Dokumente Ihrer Anwendung werden in iTunes angezeigt
12.2
Dokumente dauerhaft speichern In unserer Feelings-App konnte man Text und Emoticons in einen TextView eingeben. Wurde die Anwendung beendet und neu gestartet, wurde wieder eine leere Zeichenfläche angezeigt. In den meisten Anwendungen 1 Sie können Dateien auch per Bonjour über das Netzwerk versenden, sich selbst als EMails zusenden und sie auf dem iPad öffnen oder in Safari herunterladen. Diese Dinge gehen über den Horizont dieses Buchs hinaus.
W
229
230 X Kapitel 12: Mit Dokumenten arbeiten will man seine Arbeiten aber so speichern, dass man sie am letzten Punkt wieder aufnehmen kann, wenn die Anwendung neu gestartet wird.2 Auf dem iPad tun Sie das wie auf jedem anderen Computer. Sie müssen einen Pfad zum Dokumentort angeben, und Sie benötigen ein Mittel, Dateien zu lesen und zu schreiben. Was auf dem iPad anders als in anderen Umgebungen ist, in denen derartige Funktionen möglich sind, sind die Erwartungen Ihrer Benutzer. Auf dem iPad erwarten Leute, dass Sie das Dokument bei der Eingabe nicht kontinuierlich manuell speichern müssen. Sie erwarten, dass Sie sich für sie darum kümmern. In der Feelings-App sollten Sie die Arbeit des Benutzers wahrscheinlich immer speichern, wenn der Anwender die Tastatur schließt. Dieser Akt sagt: “Für den Augenblick bin ich mit der Arbeit fertig.” Darüber hinaus ist es natürlich sinnvoll, die Arbeit zu speichern, wenn der Nutzer die Anwendung beendet. Betrachten Sie die Sache einmal aus der Perspektive des Anwenders. Er startet Ihre Anwendung, gibt etwas in den Text-View ein, drückt, wenn er fertig ist, auf den Home-Knopf und beendet Ihre App. Wahrscheinlich erwartet er, dass seine Arbeit gespeichert wird. Öffnen Sie die Version des Feelings-Projekts /code/Files/Feelings7. Sie sehen, dass wir eine neue Klasse namens MyFileManager eingeführt und als Delegate des Text-View eingerichtet haben, die sich um das Speichern und Öffnen der Dateien kümmert. Wenn der Text-View den First Responder abgibt, wird textViewShouldEndEditing: aufgerufen.3 Wird die Anwendung beendet, ruft das App-Delegate die Methode applicationWillTerminate: auf. In beiden Fällen speichert das MyFileManager -Objekt das aktuelle Dokument. Files/Feelings7/Classes/MyFileManager.m
-(void) saveDocument:(UITextView *) textView { NSError *error1 = nil; [textView.text writeToFile:[self fileLocation] atomically:YES encoding:NSUTF8StringEncoding error:&error1]; } 2 Wenn Sie mit mehreren Dokumenten arbeiten, müssen Sie eine Galerie oder ein anderes Mittel präsentieren, über das der Anwender das gewünschte Dokument auswählen oder ein neues erstellen kann, aber die restlichen Einzelheiten bleiben unverändert. 3 Rufen Sie sich nochmal in Erinnerung, dass ein Textfeld, wenn es ausgewählt wird, First Responder wird und für die Aufnahme von Tastatureingaben bereit ist. Deswegen wird die Tastatur geöffnet. Wenn der Anwender keine weiteren Eingaben für das Textfeld machen will, sollte es den First Responder abgeben, damit die Tastatur wieder geschlossen wird.
Dateitypen registrieren
W
231
-(BOOL)textViewShouldEndEditing:(UITextView *)textView { [self saveDocument:textView]; return YES; } -(void)applicationWillTerminate:(UITextView *)textView{ [self saveDocument:textView]; }
Beim Start der Anwendung ruft das App-Delegate die Methode applicationDidFinishLaunching: auf, die die Datei Mood.txt lädt, wenn es sie gibt, und ihren Inhalt im Text-View anzeigt. Files/Feelings7/Classes/MyFileManager.m
-(void)applicationDidFinishLaunching:(UITextView *) textView { NSError *error2; if ([[NSFileManager defaultManager] fileExistsAtPath:[self fileLocation]] ) { textView.text = [NSString stringWithContentsOfFile:[self fileLocation] encoding:NSUTF8StringEncoding error:&error2 ]; } }
Wir haben jetzt eine Anwendung, die mit reinen Textdateien arbeiten kann. Angenommen, wir schreiben eine weitere Anwendung und geben ihr eine Textdatei, die gelesen werden soll, dann können wir die Feelings-App aushelfen lassen. Dazu sind drei Schritte erforderlich: 1. Wir werden dem System mitteilen, dass die Feelings-App mit reinen Textdateien umgehen kann. Das erreichen wir mit einer einfachen Ergänzung in der PList. 2. Wir werden die Feelings-App so verändern, dass sie Textdateien öffnet, die ihr übergeben werden. 3. Wir schreiben eine neue Anwendung, die um Unterstützung bitten kann, wenn sie auf einen Typen trifft, mit dem sie nicht umgehen kann. Wir werden diese Punkte nacheinander abarbeiten.
12.3
Dateitypen registrieren Der erste Schritt besteht darin, beim System zu registrieren, dass Feelings einfache Textdokumente öffnen kann. Folgenden Eintrag können Sie mit einem Texteditor oder dem PList-Editor in FeelingsInfo.plist einfügen.
232 X Kapitel 12: Mit Dokumenten arbeiten CFBundleDocumentTypes <array> LSItemContentTypes <array> <string>public.plain-text
Beachten Sie, dass der Wert, der dem Schlüssel CFBundleDocumentTypes zugeordnet ist, ein Array mit Dictionaries ist. Jedes Dictionary wird genutzt, um einen Dokumenttyp anzugeben, den die Anwendung öffnen kann. Hier geben wir den Klartexttyp über einen Uniform Type Identifier (UTI) an.4 Wir haben nur den Schlüssel LSItemContentTypes verwendet. Wir hätten ebenfalls den Schlüssel CFBundleTypeIconFiles nutzen können, um PNG-Dateien anzugeben, die für die Dateisymbole verwendet werden. Wenn Sie eigene Typen erstellen wollen, sollten Sie einen Blick in Apples iOS Application Programming Guide [App10a] werfen. Entfernen Sie die bestehende Version der Feelings-App von Ihrem iPad, erstellen Sie die Anwendung und führen Sie sie mit dem Gerät als Ziel aus. Feelings sollte starten wie zuvor. Sie dürften an der Anwendung keinen Unterschied sehen. Wir haben in keiner Weise geändert, wie Feelings funktioniert – wir haben nur geändert, wie andere Anwendungen Feelings sehen. Wenn Sie sich diese Änderung ansehen wollen, erstellen Sie mit TextEdit eine Textdatei und mailen diese sich selbst als Anhang. Rufen Sie mit Ihrem iPad Ihre Mails ab und wählen Sie den Anhang. Mail sollte Ihnen einen View wie den in der Abbildung mit einem Öffnen in ...-Button in der oberen rechten Ecke präsentieren. Tippen Sie auf den Button, sollten Sie in etwa Folgendes sehen:
4 http://developer.apple.com/library/ios/#documentation/FileManagement/Conceptual/understanding_utis/understand_utis_intro/understand_utis_intro.html
Eine Datei beim Start öffnen Das sind die Anwendungen, die sich im System für die Verarbeitung von Klartextdateien registriert haben. Bei Ihrem System kann die Liste anders aussehen, je nachdem, welche Apps bei Ihnen installiert sind. Sie müssten jetzt allerdings die Feelings-App in der Liste sehen. Das ist Schritt 1. Wenn wir Schritt 2 hinter uns haben, können Sie die Textdatei in der Feelings-App bearbeiten. Noch öffnet sich Feelings mit dem Inhalt, den die Datei Mood.txt enthielt, als die App das letzte Mal ausgeführt wurde.
12.4
Eine Datei beim Start öffnen Entschließt sich der Anwender, die Datei mit der Feelings-App zu öffnen, werden die zu öffnende Datei und ein Dictionary an die App übergeben. Diese Informationen nutzen wir in der application:didFinishLaunchingWithOptions:-Methode im FeelingsAppDelegate. Vielleicht ist das das erste Mal, dass Sie Optionen nutzen, wenn diese Methode aufgerufen wird. Apple sagt eindeutig, dass die Anwendung, sobald eine Datei geöffnet werden soll, diese Datei unmittelbar öffnen und dem Benutzer anzeigen soll. Files/Feelings9/Classes/FeelingsAppDelegate.m
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [self.window addSubview:self.viewController.view]; [self.window makeKeyAndVisible]; X [self.myFM applicationDidFinishLaunchingWithFileURL: X [launchOptions valueForKey:UIApplicationLaunchOptionsURLKey] X inTextView:self.viewController.textView]; return YES; }
Hier lesen wir den NSURL, der dem zu öffnenden Dokument entspricht, aus dem Options-Dictionary. Wenn wir irgendwann erneut die aufrufende Anwendung aufrufen wollen, können wir ihre Bundle-ID aus UIApplicationLaunchOptionsSourceApplicationKey herausziehen. Müssen mehr Informationen übergeben werden, befinden sich diese als Einträge in einem Dictionary, das dem Schlüssel UIApplicationLaunchOptionsAnnotationKey entspricht. In unserem Fall könnten wir dazu Folgendes in der Klasse MyFileManager tun: Files/Feelings9/Classes/MyFileManager.m
-(void)applicationDidFinishLaunchingWithFileURL:(NSURL *)fileURL inTextView:(UITextView *) textView {
W
233
234 X Kapitel 12: Mit Dokumenten arbeiten NSError *error2; textView.text = [NSString stringWithContentsOfURL: fileURL encoding:NSUTF8StringEncoding error:&error2 ]; }
Natürlich sollten wir den Fehler prüfen und zumindest während der Entwicklung anzeigen. Der Punkt hier ist, dass wir die Datei öffnen, die uns vom System übergeben wird, als die App die Anfrage erhielt, diese Datei zu öffnen. 5 Erstellen Sie die Anwendung, kompilieren Sie sie und beenden Sie dann die Feelings-App. Versuchen Sie jetzt, das Klartextdokument aus Mail mit der Feelings-App zu öffnen. Die Mail-App sollte beendet werden, stattdessen soll die Feelings-App starten und den Inhalt der Textdatei im Text-View anzeigen.
12.5
Dateien öffnen Wenn Ihre App auf einen Dateityp trifft, den sie nicht verarbeiten kann, müssen Sie ihr eine Instanz eines UIDocumentInteractionController geben. So wusste auch die Mail-App, wie sie dem Nutzer die Möglichkeit bieten konnte, das Textdokument mit der Feelings-App zu öffnen. Erstellen wir eine einfache Anwendung, die nichts tut, als andere Apps um Unterstützung bei der Arbeit mit Dokumenten zu bitten. Erstellen Sie eine neue View-basierte iPad-App namens SingleFile. Fügen Sie SingleFile-Info.plist Folgendes hinzu, um eine Synchronisierung mit iTunes zu ermöglichen: UIFileSharingEnabled <true/>
Erstellen Sie die App und führen Sie sie auf dem Gerät aus. Beenden Sie SingleFile und synchronisieren Sie das iPad. Sie sollten SingleFile in iTunes jetzt Dokumente hinzufügen können. Fügen Sie ihr das zuvor erstellte Textdokument hinzu. Wir werden in diesem Buch den Namen Mood.txt verwenden, aber wenn Sie wollen, können Sie auch einen anderen Namen vergeben. Wenn SingleFile startet, werden wir versuchen, die Datei Mood.txt zu öffnen. Unglücklicherweise hat SingleFile keine Ahnung, was sie mit einem Klartextdokument anstellen soll. Die App braucht Unterstützung, und genau da tritt der UIDocumentInteractionController auf 5 Beachten Sie, dass auch der Code zum Speichern des Dokuments entfernt wurde, weil er nicht mehr Teil dessen ist, was wir uns hier ansehen wollen.
Dateien öffnen
W
235
den Plan. Wir nutzen einen für jedes Dokument. Deklarieren Sie in der View-Controller-Header-Datei eine Eigenschaft des Typs UIDocumentInteractionController: Files/SingleFile1/Classes/SingleFileViewController.h
#import @interface SingleFileViewController : UIViewController { } @property (nonatomic, retain) UIDocumentInteractionController *controller; -(void)applicationDidFinishLaunching; @end
Außerdem haben wir die Methode applicationDidFinishLaunching: deklariert, die Sie am Ende der application:didFinishLaunchingWithOptions:-Methode des App-Delegates aufrufen müssen. Den controller initialisieren wir verzögert im Getter. Files/SingleFile1/Classes/SingleFileViewController.m
-(UIDocumentInteractionController *) controller { if (controller == nil) { NSString *applicationDocumentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject]; NSString *fileLocation = [applicationDocumentsDirectory stringByAppendingPathComponent:@"Mood.txt"]; NSURL *fileURL = [NSURL fileURLWithPath:fileLocation]; X self.controller = X [UIDocumentInteractionController interactionControllerWithURL:fileURL]; } return controller; }
Der Hauptteil des Codes kümmert sich um die Erstellung der URL für Mood.txt. Wenn Ihr Text einen anderen Namen hat, müssen Sie hier diesen anstelle von Mood.txt verwenden. Wir initialisieren den Controller mit der Klassenmethode interactionControllerWithURL. Nachdem wir unseren UIDocumentInteractionController nun haben, können wir ihn für drei elementare Dinge nutzen: Wir können dem Benutzer ein Popover einblenden, das die Anwendungen anzeigt, die eine Datei entsprechenden Typs öffnen können, in einem modalen View-Controller eine Dokumentvorschau anzeigen oder dem Benutzer anzeigen, welche Optionen es gibt (Öffnen oder Vorschau). Nur die verfügbaren Optionen werden angezeigt. In unserer Anwendung werden wir beispielsweise ein Popover zum Öffnen von Mood.txt
236 X Kapitel 12: Mit Dokumenten arbeiten anzeigen. Ist keine Anwendung verfügbar, die bekannt gemacht hat, dass sie Klartextdateien öffnen kann, wird das Popover nicht angezeigt. Hier ist der Code, mit dem wir das Popover zum Öffnen von Mood.txt anzeigen: Files/SingleFile1/Classes/SingleFileViewController.m
-(void)applicationDidFinishLaunching { [self.controller presentOpenInMenuFromRect:CGRectMake(200, 200, 200, 200) inView:self.view animated:YES]; }
Erstellen Sie die App und führen Sie SingleFile auf dem Gerät aus. Sie sollten folgendes Popover sehen, wenn die App gestartet wird:
Öffnen Sie die Datei in Feelings. SingleFile wird beendet, Feelings wird gestartet, und Ihr Dokument wird angezeigt. 6
Sie können diese Schritte auch im Simulator nachvollziehen. Erstellen Sie die App und führen Sie Feelings auf dem Simulator aus. Beenden Sie die App dann. Erstellen und führen Sie auch SingleFile aus. Noch sollte nichts passieren. Dem Simulator können Sie Mood.txt nicht über iTunes 6 Sie können das Textfeld anwählen, um das Dokument zu bearbeiten, aber aktuell werden keine Änderungen gespeichert.
Dateivorschau
W
hinzufügen, aber Sie können die Datei direkt in das Documents-Verzeichnis in ~/Library/Application\ Support/iPhone\ Simulator/3.2/Applications kopieren. Erstellen Sie die App und führen Sie sie aus. Jetzt sollten Sie das Popover sehen, das wir auch auf dem Gerät sahen. Kopieren Sie ein PDF in das Verzeichnis. Beispielsweise könnten Sie das PDF für dieses Buch nutzen, wenn Sie es zur Verfügung haben. Geben Sie dem PDF den Namen iPadBook.pdf und ändern Sie die controller Methode so, dass statt Mood.txt diese Datei geladen wird. Wenn Sie die App jetzt erstellen und ausführen, erscheint kein Popover. Auf dem Gerät gibt es keine Anwendung, die für das Öffnen von PDFs registriert ist.
12.6
Dateivorschau PDFs können wir nicht öffnen, aber wir können eine Vorschau anzeigen lassen, ohne Single-File zu verlassen. Dazu müssen wir den Aufruf von presentOpenInMenuFromRect:inView:animated: in presentPreviewAnimated: ändern: Files/SingleFile2/Classes/SingleFileViewController.m
-(void)applicationDidFinishLaunching { self.controller.delegate = self; [self.controller presentPreviewAnimated:YES]; }
Sie sehen, dass wir auch das Delegate für den UIDocumentInteractionController setzen müssen. Als wir das Dokument öffneten, mussten wir das nicht tun, aber wenn wir eine Vorschau eines Dokuments anzeigen, müssen wir den View-Controller angeben, der der Eltern-View des modalen View ist, der angezeigt wird. Das tun wir in folgender Delegate-Methode: Files/SingleFile2/Classes/SingleFileViewController.m
-(UIViewController*)documentInteractionControllerViewControllerForPreview: (UIDocumentInteractionController *)controller { return self; }
Fügen Sie der Header-Datei die Deklaration des UIDocumentInteractionControllerDelegate-Protokolls hinzu. Erstellen Sie die App und führen Sie sie aus. Sie sollten jetzt eine vollständige Vorschau des PDF sehen.
237
238 X Kapitel 12: Mit Dokumenten arbeiten
12.7
Zusammenfassung Denken Sie daran, dass einer der wichtigsten Aspekte der Magie des iPads ist, dass es sich dem Benutzer nicht in den Weg stellt. Die in diesem Kapitel vorgestellten Techniken erleichtern Ihren Benutzern den Austausch von Dokumenten zwischen Ihrer Anwendung und Ihrem Computer, ohne dass sie dazu etwas über das System wissen müssen. Sie haben gelernt, wie Sie Ihre Anwendung dazu bringen, mit anderen Anwendungen zusammenzuarbeiten, um Vorschauen für andere Dokumenttypen anzuzeigen oder andere Dokumenttypen zu öffnen. iPad-Anwendungen sind meist etwas komplexer als iPhone-Anwendungen und weniger komplex als Mac OS X-Anwendungen. Wenn Sie für das iPad schreiben, könnte es hilfreicher sein, eine Suite kleiner Anwendungen anstelle einer großen, komplexen Anwendung zu schreiben. Die Techniken in diesem Kapitel vereinfachen Ihnen den Austausch von Dokumenten gemäß den Bedürnissen des Benutzers.
Kapitel 13
Die große Zusammenfassung Herzlichen Glückwunsch. Sie haben sich eine Menge der Haken und Ösen in der iPad-Programmierung angesehen, während Sie sich den unterschiedlichen Themen gewidmet haben, die in diesem Buch behandelt werden. Wir haben eine große Bandbreite von iPad-Themen besprochen, von Split-View-Controllern über externe Anzeigegeräten bis hin zu der neuen Gesten-API zur Kommunikation zwischen iOSGeräten. Viele dieser Dinge wurden mit der Markteinführung des iPads vorgestellt und sind mit wenigen Ausnahmen ab iOS 4 (und natürlich auch unter iOS 4.3) auf anderen iOS-Geräten verfügbar. Die meisten Techniken, die Sie in diesem Buch gelernt haben, lassen sich auf alle iOS-Geräte anwenden. Im gesamten Buch haben wir uns darauf konzentriert, wie Sie die Dinge auf technischer Ebene machen – wie man einen Film zeigt, wie man ein Popover nutzt und so weiter. Lassen Sie uns jetzt einen Schritt zurücktreten und die Dinge von einer höheren Warte aus betrachten. Dieses Kapitel enthält die Top Ten der Dinge, die Sie im Kopf haben sollten, wenn Sie iPad-Anwendungen aufbauen. Wir werden dieses Kapitel und damit das ganze Buch schließen, indem wir uns bei einigen der vielen Menschen bedanken, die uns bei diesem Projekt unterstützt haben.
240 X Kapitel 13: Die große Zusammenfassung
13.1
Denken Sie immer zuerst an den Benutzer Bevor Sie eine einzige Zeile Code schreiben oder eine erste Nib erstellen, müssen Sie eine klare Vorstellung davon haben, für wen die Anwendung gedacht ist. Apples Empfehlung für iPhone-Apps ist, dass Sie eine klare und verständliche Produktbeschreibung bieten. Dieser Rat ist bei iPad-Apps genauso wichtig. Sie müssen Ihr Publikum kennen und wissen, welchen spezifischen Zweck Ihre Anwendung erfüllen soll. Das wird Ihnen helfen, die Funktionen herauszufinden, die eingeschlossen werden sollten und – was ebenso wichtig ist – die Sie sich sparen können. Ein fundiertes Wissen über Ihre Benutzer wird Ihnen die Entscheidung darüber erleichtern, ob Sie eine universelle App, eine iPad-spezifische App oder zwei Apps erstellen sollen – eine für das iPhone und eine für das iPad. Wenn Sie nicht genau wissen, was Ihr Benutzer erwartet und was für Bedürfnisse er hat, geschieht es leicht, dass Sie zu viele Funktionen integrieren. Einer Ihrer Betatester wird bestimmt sagen: „Also, es wäre echt cool, wenn Sie das noch ergänzen könnten.“ Vielleicht wäre es gar kein so großes Problem, die vorgeschlagene Funktion einzubauen, und vielleicht ist sie auch so cool, wie Ihr Bekannter meint. Aber wenn sie nicht in die Kernbeschreibung des Funktionsumfangs Ihrer App passt, kann sie die Arbeit mit Ihrer App erschweren und sie so weniger ansprechend für Ihr Zielpublikum machen.
13.2
Behandeln Sie Landscape- und Portrait-Modus gleichrangig Greifen Sie Ihr iPhone mit einer Hand. Egal, ob Sie es ans Ohr halten, um einen Anruf zu tätigen oder etwas in einem Table-View nachschlagen: Sie halten es wahrscheinlich im Portrait-Modus. Auf dem iPhone ist der Portrait-Modus die bevorzugte Ausrichtung – zumindest im Augenblick. Das iPad ist ganz anders. Es gibt keine natürliche Ausrichtung für das Gerät. Gut, es gibt nur einen Home-Button, aber es gibt nichts, was den Benutzer auffordert, das Gerät bei der Nutzung so zu halten, dass dieser Button unten ist. Und es gibt auch Fälle, die standardmäßig eine Landscape-Ausrichtung mit dem Home-Button auf der linken Seite erfordern.
Die Hierarchie glätten Sie müssen in Ihren Anwendungen Landscape- und Portrait-Modus unterstützen. Sie müssen dem Benutzer ermöglichen, die Anwendung in beiden Ausrichtungen zu öffnen, und Sie müssen das Beste aus der Ausrichtung machen, für die er sich bei der Interaktion mit Ihrer App entscheidet.
13.3
Die Hierarchie glätten Wahrscheinlich haben Sie sich bereits eine Menge Tricks für die iPhone-Entwicklung zugelegt. Für das Telefon waren diese gut geeignet, trotzdem müssen Sie jetzt einige Ihrer Design-Entscheidungen überdenken. Ein fundamentaler Kompromiss, den Sie bei der iPhone-Entwicklung eingehen mussten, waren tiefe Hierarchien. Auf einem Telefon konnten Sie immer nur kleine Datenmengen anzeigen und lernten deswegen, wie man Tab-Leisten und Nav-Controller verwendet, um den Benutzern bei der Navigation von Folie zu Folie zu den Informationen zu verhelfen, die sie benötigen. Im Prinzip funktionieren diese Techniken auch auf dem iPad noch. Trotzdem sollten Sie Ihre Anwendung in Bezug auf Design und Nutzbarkeit überdenken. Sie können in einem Fenster jetzt erheblich mehr Informationen unterbringen, und das auf eine Weise, die für den Benutzer deutlich leichter zu verstehen ist. Wir sahen in einem Beispiel, wie ein Navigation-Controller für das iPhone durch einen Split-View für das iPad ersetzt wurde. In der iPhone-Version wählte der Benutzer eine Zeile, und der Table-View wurde durch einen Detail-View ersetzt. Wollte der Benutzer zurückgehen, um sich die weiteren Optionen anzusehen, musste er auf einen Zurück-Button auf dem Detail-View tippen, um zum Table-View zurückzukehren. Auf dem iPad konnten wir den Split-View nutzen, um auf der linken Seite den Table-View und auf dem restlichen Bildschirm den DetailView anzuzeigen. Der Benutzer konnte eine der Optionen wählen und sich die Informationen ansehen, ohne dass er den direkten Zugriff auf die anderen Optionen verlor. Im Portrait-Modus stecken die Optionen in einem Popover, das der Benutzer mit einem Tipp auf einen Button öffnen kann, ohne dass die aktuell angezeigten Daten verschwinden. Achten Sie also auch in Ihren Anwendungen darauf, dass die Hierarchie flach bleibt.
W
241
242 X Kapitel 13: Die große Zusammenfassung
13.4
Erstellen Sie detailreiche, realistische Views Wenn Sie bereits vor der Ankunft des iPads für Apple-Plattformen entwickelt haben, wissen Sie, dass Apple üblicherweise empfiehlt, sich an Apples UI-Richtlinien zu halten, Standard-UI-Komponenten zu nutzen und nur in Extremfällen eigene Views zu erstellen. Diese Empfehlungen haben sich mit dem iPad geändert, Apple ermutigt Sie jetzt dazu, eigene Schnittstellen aufzubauen, die einen realistischeren Anstrich haben. Sie müssen sich nur Dinge wie iBooks, den Kalender oder viele andere externe Anwendungen ansehen, um sich einige wunderbare Beispiele dafür vor Augen zu führen. Wenn Sie Ihre Anwendung entwerfen, sollten Sie erwägen, Views zu erstellen, die das lebensweltliche Gegenstück zu Ihrer Anwendung widerspiegeln (vorausgesetzt, es gibt ein solches). Beauftragen Sie gegebenenfalls einen Designer. Das soll nicht heißen, dass Sie die Standardkomponenten nicht verwenden sollen, solange sie sinnvoll sind: Diese helfen Ihren Benutzern, sich in Ihrer Anwendung zurechtzufinden.
13.5
Gesten sind mächtig Ab iOS 3.2 (4.0 für das iPhone) bietet Apple eine mächtige neue API, über die man im Handumdrehen verbreitete Gesten wie Tap-, Pan-, Pinch-, Hold-, Rotation- und Swipe-Gesten in Anwendungen integrieren kann. Studieren Sie diese sorgfältig und achten Sie darauf, dass Sie sie auf eine Weise nutzen, die der Erwartungshaltung Ihrer Benutzer entspricht (iOS Human Interface Guidelines [App10c]). Sie sollten unbedingt vermeiden, Gesten auf eine unkonventionelle, verwirrende Weise zu nutzen. Darüber hinaus gibt Ihnen Apples neue API ebenfalls die Möglichkeit, eigene Gesten zu erstellen, und auch wenn diese sparsam genutzt werden sollten, ist das Gestenvokabular noch lange nicht ausgereift. Wer sagt denn, dass Sie nicht derjenige sein könnten, der die neue Geste entwickelt oder zumindest eine, die für die Zwecke Ihrer Anwendung geeignet ist?
13.6
Das iPad will kommunizieren Irgendwann in der Entwicklungszeit sollten Sie sich fragen, was sich ändert, wenn Ihre Anwendung mit anderen Instanzen der gleichen Anwendung kommunizieren kann. Vielleicht wird eine Anwendung, mit
Dokumente der man Dokumente erstellen kann, zu einer Anwendung, mit der man auch gemeinsam an Dokumenten arbeiten kann. Vielleicht wird eine Anwendung, mit der man seine Gedanken festhalten kann, zu einer Anwendung, mit der man Gedanken austauschen und vergleichen kann. Sie haben in diesem Buch gesehen, dass die Mechanismen für die Kommunikation zwischen Geräten recht einfach ist. Sie entscheiden, ob das Verhältnis zwischen den Instanzen eine Client/Server- oder eine Peerto-Peer-Beziehung ist, und antworten dann auf Delegate-Methoden. Das Schwierige sind die strategischen Entscheidungen. Was soll durch die Kommunikation zwischen den Geräten geleistet werden? Die Möglichkeiten sind grenzenlos.
13.7
Dokumente Sie haben ein iPhone, ein iPad und einen Laptop – wie kompliziert kann es sein, ein auf dem einen Gerät erstelltes Dokument auf eins der anderen Geräte zu bekommen? Seit dem iOS 3.2 SDK ist die Antwort: „So schwer nicht.“ Sie können den Dokumentaustausch leicht über die Synchronisierung mit iTunes erreichen. Wir haben uns auch angesehen, wie man ein Ökosystem von Anwendungen erstellt, die mit unterschiedlichen Dokumenttypen umgehen können. Sie sahen, wie leicht man dem System mitteilen kann, dass Ihre Anwendung bestimmte Arten von Dokumenten unterstützt, und wie man Ihre Anwendung dazu bringt, andere Anwendungen zu nutzen, um Dokumente zu öffnen, die Ihre Anwendung nicht unterstützt.
13.8
Video ist wichtig Wenn die Videoeinbettung bei Ihrer Anwendung sinnvoll ist, haben Sie jetzt mit unmittelbar in Views eingebetteten Videos die Werkzeuge zur Gestaltung überzeugender Erlebnisse. Darüber hinaus gibt Ihnen die Video-API alle Funktionalitäten, die Sie benötigen, um Ihre Schnittstelle mit dem Video zu synchronisieren und umgekehrt. Überlegen Sie sich, wie Sie mit Ihren Videos über das „Video in einer Webseite“-Modell hinausgehen und echte interaktive Erlebnisse anbieten können – inklusive zusätzlicher Informationen, Kommentar- und anderer sozialer Funktionen.
W
243
244 X Kapitel 13: Die große Zusammenfassung
13.9
Externe Anzeigegeräte verlangen eine angepasste Implementierung Auch wenn man sein iPad gern an einen externen Monitor anschließen und die Anzeige des iPads auf diesem spiegeln möchte, geht das nicht. Wenn Sie Inhalte auf einem zweiten Bildschirm anzeigen wollen, müssen Sie eine separate View-Hierarchie erstellen und verwalten. Obgleich das nach unnötigen Mühen klingt, können Sie es zu Ihrem Vorteil nutzen. Beispielsweise könnten Sie in Ihrer Sport-Anwendung das Bild auf den großen Bildschirm leiten, während auf dem iPad die Kameraperspektiven gewechselt und die Spielstatistiken angezeigt werden. Oder spielen Sie Gameshow-Moderator: Auf dem großen Bildschirm werden die Fragen eingeblendet, während Sie das Spiel von Ihrem iPad steuern. Nutzen Sie die externe Anzeige kreativ!
13.10
Verbessern Sie die Leistungen Ihrer App mit VideoStreaming Bei Ihrer Video-Anwendung müssen Sie auch überlegen, ob die Videos direkt in das App-Bundle eingeschlossen werden können oder Ihrer App zugestreamt werden müssen. Wenn Sie streamen müssen, sollten Sie sich mit den Einzelheiten der Segmentierung und des Hostings für die Unterstützung von Apples HLS-Protokoll befassen, insbesondere wenn Sie hochwertige Inhalte anbieten wollen. Wenn man Inhalte für HLS vorbereiten will, muss man einiges lernen. Aber diese Mühen werden dadurch belohnt, dass sich Ihr Video-Player dann an die verfügbare Bandbreite anpasst, selbst wenn sich diese mit der Zeit ändert.
13.11
Danksagungen Dieses Buch hat uns beiden eine Menge Spaß gemacht. Wir danken den technischen Fachgutachtern und den Lesern, die Fehlermeldungen einsandten. Diese Rückmeldungen waren ungemein hilfreich. Unser Dank geht insbesondere an Kim Shrier, die die Betaversion des Buchs erwarb und uns über den Betaprozess hinweg ausgezeichnete Vorschläge zukommen ließ. Janine Sisk gab uns im Zuge der technischen Begutachtung ausführliche Ratschläge, die die endgültige Version enorm verbessert haben. Carlos A. Weber, MD und Paul Lynch
Danksagungen W 245 erstellten ausgezeichnete technische Gutachten, und auch Elisabeth Robson und Joe Heck halfen, die Qualität des Buchs zu verbessern. Unser Dank geht auch an Chris Adamson und Craig Castelaz, zwei Freunde, die immer einen Augenblick Zeit haben, wenn man über Objective-C-Code sprechen will. Dank ebenfalls an Bill Dudney und Eryk Vershen, die wertvolle Hilfe bei technischen Fragen gaben.
Daniel Zunächst möchte ich meinem Mitautor, Eric Freeman, danken. Eric und ich kennen uns bereits eine lange Zeit und haben hier und da schon gemeinsam an Projekten gearbeitet, aber noch nie gemeinsam ein Buch geschrieben. Es war extrem angenehm, mit ihm zusammenzuarbeiten, und obwohl wir die Arbeit am Buch im Wesentlichen kapitelweise aufgeteilt hatten, war das ein sehr kollaboratives Projekt. Ich freue mich schon darauf, eines Tages mit Eric an einem anderen Buch zu arbeiten. Dank an Mike und Nicole Clark, die Eigentümer von Pragmatic Studio (http://pragmaticstudio.com). Ich habe in den letzten Jahren so viel gelernt, während ich die Kurse zur Programmierung auf dem Mac, dem iPhone und jetzt dem iPad gab. Aktuell gebe ich Kurse zusammen mit Matt Drance, früher mit Bill Dudney. Beide sind absolute Rockstars, von denen ich eine Menge gelernt habe (was man auch daran sieht, dass Bill den Evangelisten-Posten bei Apple übernahm, den Matt aufgegeben hatte). Dank an Kimmy-the-wonderwife, die immer die Beste bleibt. Die Stunden mit Kim und Maggie, unserer Tochter, sind mir die liebsten Zeiten am Tag. Meine Schreibgefährtin ist Anabelle, unser 2½ Jahre alter Deutsch-Kurzhaar-Schwarzer-Labrador-Mischling. Sie blickt über meine Schulter, während ich im Hinterhof am Picknicktisch sitze und Code und Prosa schreibe. Wenn sie wegguckt, spüre ich, dass ich ihre Aufmerksamkeit verloren habe, und schreibe den entsprechenden Absatz neu. Abschließend möchte ich meinen Freunden und Kollegen bei den Prags danken. Dort ist seit vier Jahren meine Heimat, und ich habe es stets genossen, mit ihnen Bücher herauszugeben und zu schreiben. Ich werde die Arbeit mit ihnen vermissen. Meine neuesten Projekte finden Sie immer unter: http://dimsumthinking.com.
246 X Kapitel 13: Die große Zusammenfassung
Eric Großer Dank geht an meinen Mitautor, Daniel Steinberg. Ich wollte schon immer etwas zusammen mit Daniel auf die Beine stellen und bin froh, dass wir ein so spannendes gemeinsames Projekt gefunden haben. Außerdem möchte ich Dan danken, dass er mich wieder zur Cocoa-Programmierung zurückgeführt hat. Ich hatte selten so viel Spaß und warte hoffnungsfroh auf weitere gemeinsame Projekte und Spin-offs. Dank auch meinen Freunden bei den Prags, Dave Thomas und Andy Hunt. Derart handliche und gleichzeitig mächtige Produktionswerkzeuge sind mir bislang noch nicht über den Weg gelaufen, und ich habe es wirklich genossen, mit ihnen an diesem Buch zu arbeiten. Besonderer Dank ebenfalls an Dave, der einsprang, als wir auf Lektoratsseite zusätzliche Unterstützung benötigten. Ein besonderer Dank an meine fantastische Frau, Jenn Freeman, die schwanger war, während ich an diesem Buch arbeitete, und zwei Wochen vor Beendigung des Buchs unsere Tochter zur Welt brachte. Avary Katherine Freeman schlief häufig neben mir, als das Buch in den letzten Zügen lag. Schließlich möchte ich Ihnen, lieber Leser, danken, dass Sie zu diesem Buch gegriffen haben und nun daran mitarbeiten, fantastische Apps für außergewöhnliche Geräte zu schreiben. Sie finden mich unter http://ericfreeman.com, ich freue mich, wenn Sie vorbeischauen.
Anhang
Literaturverzeichnis [App10a]
Apple, Inc. iOS Application Programming Guide. http://developer.apple.com/library/ios/#documentation/ iPhone/Conceptual/iPhoneOSProgrammingGuide/ Introduction/Introduction.html
[App10b]
Apple, Inc. Event Handling Guide for iOS. http://developer.apple.com/library/ios/#documentation/ EventHandling/Conceptual/EventHandlingiPhoneOS/ Introduction/Introduction.html
[App10c]
Apple, Inc. iOS Human Interface Guidelines. http://developer.apple.com/library/ios/#documentation/ UserExperience/Conceptual/MobileHIG/Introduction/ Introduction.html
[App10d]
Apple, Inc. Drawing and Printing Guide for iOS. http://developer.apple.com/library/ios/#documentation/ 2DDrawing/Conceptual/DrawingPrintingiOS/ Introduction/Introduction.html
[App10e]
Apple, Inc. HTTP Live Streaming Overview. http://developer.apple.com/library/ios/#documentation/ NetworkingInternet/Conceptual/StreamingMediaGuide/ HTTPStreamingArchitecture/ HTTPStreamingArchitecture.html
[App 10f]
MPMoviePlayerController Class Reference. http://developer.apple.com/library/ios#documentation/ mediaplayer/reference/MPMoviePlayerController_Class/ MPMoviePlayerController/MPMoviePlayerController.html
248 X Anhang: Literaturverzeichnis [DA09]
Bill Dudney und Chris Adamson. Entwickeln mit dem iPhone SDK. The Pragmatic Programmers 2009, deutsche Übersetzung von O’Reilly 2010.
[Dud08]
Bill Dudney. Core Animation for OS X: Creating Dynamic Compelling User Interfaces. The Pragmatic Programmers 2008.
[GL06]
David Gelphman and Bunny Laden. Programming with Quartz, 2D and PDF Graphics in Mac OS X. Morgan Kaufman, San Francisco 2006.
[Ste09]
Daniel H. Steinberg. Cocoa-Programmierung: Der schnelle Einstieg für Entwickler. The Pragmatic Programmers 2009, deutsche Übersetzung von O’Reilly 2010.
[Tho06]
R. Scott Thompson. Quartz 2D graphics for Mac OS X developers. Pearson Education 2006.
Index A Abspieldauer, Movie-Player 170, 173 acceptConnectionFromPeer 212 Accessory-View 107–108 Adapterkabel 195 addGestureRecognizer-Methode 53 Anwendungen externe Anzeigegeräte 244 für Benutzer schreiben 240 Hierarchie und 241 Kommunikation und Zusammenarbeit 243 Kompatibilität mit anderen Geräten 243 Orientierung und 240 realistische Views und 242 Streaming-Fähigkeit 244 universell vs. gerätespezifisch 16–18 Videofunktionen 243 appendPath:-Methode 124 applicationDidFinishLaunching:Methode 231, 235 applicationDidFinishLaunchingWith Options:-Methode 37, 233, 235 applicationDocumentDirectory: -Methode 130 applicationWillTerminate:-Methode 230 assignmentAtIndex:-Methode 9 Assignments 45 AssignmentViewController 28
Ausfrufezeichen 123 Ausrichtung beim Start 21 entwerfen für 240 Frachtcontainer-App 97, 99 automatische Freigabe 83
B beginSession-Methode 215 bezierPathWithOvalInRect:-Methode 124 bezierPathWithRect:-Methode 124 Beziér-Projekt 116–120 Bilder 115–132 Dateien speichern 129–131 Entwurfsplan für Dreieck 117 komplexe Interfaces gestalten 242 Kreise und Rechtecke zeichnen 123, 129 mit Core Graphics zeichnen 116–120 mit der Cocoa API zeichnen 120–123 Versionen von 115 Bildschirm Ausrichtung 21 Größe 18 Portrait- vs. Landscape-Modus 18, 20 Bluetooth-Fähigkeit 211 Buttons 105
250 X CargoColorChooser 85
C CargoColorChooser 85 Cargo-Container Touch und 83 cargoContainerDidGetTappedMethode 83, 91 centerContext:-Methode 119 CGContext-Objekt 117 CGMutablePath-Objekt 117 CGPathAddLineToPoint-Methode 118 CGPathCloseSubpath-Methode 118 CGPathMoveToPoint-Methode 118 CGRectMake-Methode 130 CGRectZero-Methode 130 Chat 224 Clark, Mike 6 Cocoa API, zeichnen mit 120–123 Cocoa-Programmierung. Der schnelle Einstieg für Entwickler (Steinberg) 5–6, 109 Code für Anwender schreiben 240 schreiben vs. warten 33 configureView 46 connectToAvailablePeer:-Methode 214 connectToPeer:-Methode 213 Controller verbinden mit 32 controller-Variable 235 convertRect:fromView:-Methode 111 Core Animation for Mac OS X and the iPhone. Creating Compelling Dynamic User Interfaces (Dudney) 60, 64 Core Graphics 116–120 count-Methode 9 currentPlaybackRate 142 currentPlaybackTime 142
D Dateien öffnen 233–237 Dateitypen registrieren 231–233 Dateivorschau 237 Daten empfangen 217–218 senden 218–220
Dauer 142 Davidson, James Duncan 6 Delegate-Methoden 13 Delegates 42 Detail-View Interaktion mit 27 Toolbar hinzufügen zu 39–40 didTapAngryKey-Methode 107 disconnectFromAllPeers-Methode 220 diskrete Gesten 61, 65–66 DNS-SD-Registrierung 211 Dokumente 227 Dateitypen registrieren 231–233 Gerätunterstützung für 243 öffnen 233–237 speichern 230–231 Überblick 227, 238 übertragen mit iTunes 228–229 Vorschau 237 Dokumente dauerhaft machen 230 Downloads 5 drawInCurrentContext-Methode 130 drawRect:-Methode 116–120, 122–123, 129
E eigene Gesten 65, 75 Emoticons 104 Entwickeln mit dem iPhone SDK (Adamson) 6 Entwickler Anwender und 240 Anwendungswahl 16 Gesten und 242 Orientierung und 240 realistisches Design und 242 registrieren als 5 Entwickler, sich registrieren als 5 ExternalDisplayViewControllerKlasse 195 externe Anzeigegeräte 195–207, 244 einfache Ausgabe auf 200 entfernen während Nutzung 201 erkennen 196–199 Überblick 195 Videoinhalte und 204–206
iPad W 251
F FeelingsViewController 106 Finder 228 Flip-Animation 60 Füllung 119, 122
G Game Kit 211 Geräte verbinden 209–225 chatten 224 Client starten 213–215 Daten empfangen 218–220 Daten senden 217–218 Monty Hall-Problem 210–211 Peers bekannt machen 220–221 Peers verbinden 222–223 Server starten 211–213 Spielende (aufräumen) 220 Spiellogik einfügen 215–217 Überblick 225 Geräusch, Luftpolsterfolie knallen lassen 76–77 Gesten 49–80 automatisch freigeben 83 erstellen 156 Knallgeräusch 76–77 Konflikt 78–80 kontinuierliche 66 Möglichkeiten von 242 Recognizer und 52 selbstdefinierte 65, 75 Überblick 49, 80 virtuelle Luftpolsterfolie 50–51 kontinuierliche Gesten 61–65 Multi-Touch-Events 58 Taps 52–55 UISwipeGestureRecognizer 58–61 getInfo:-Methode 141, 145 getInfo-Button 140
H Hall, Monty 210 handleControlsTimer:-Methode 180
handleLoadStateDidChange:Methode 193 handlePinchFrom:-Methode 63 handlePlayAndPauseButton:Methode 181 handleTapFrom:-Methode 53, 56, 62, 156, 179 hideThePrize:-Methode 219 Hierarchien abflachen 241 HTTP Live Streaming Overview (Apple) 187 HTTP Live Streaming protocol (HLS) 186, 188–189
I IBOutlet 135 Icons erstellen 19 für Spotlight 20 Größe von 18 if-Anweisungen 33 indexPath 148 Info Light 140 init 137 initWithPlayer:-Methode 171–172, 182 initWithSessionID:displayName: sessionMode:-Methode 211 initWithTarget:action:-Methode 52 Instanzvariable 7 interactionControllerWithURLMethode 235 invokeMethodForKey:-Methode 219 iOS Human Interface Guidelines (Apple) 88 iOS Programming Guide (Apple) 80, 128, 131 iPad Adapterkabel und 195 Größe von 4 Icon-Größe 18 Kommunikation und Zusammenarbeit 243 Medien und 133 vs. iPhone 1, 4, 15 vs. iPod touch 4 vs. Laptop 2
252 X iPod touch 4 iPod touch 4 isTimeForNextShoutout-Methode 162 items-Array 146 iTunes, Dokumente übertragen mit 228–229 Ivars 7
J Jobs, Steve 1, 195
K Kay, Alan 3 keyboardWillAppear:-Methode 109, 112 keyboardWillDisappear:-Methode 112 Knallgeräusch 76–77 Kompatibilität 243 Kompatibilitätsmodus 14–15 kontinuierliche Gesten 61, 65–66 Kreise zeichnen 123, 129
L LaMarche, Jeff 132 Landscapemodus Bildschirmgröße 20 entwerfen für 240 Split-View 22 loadSelectedPage 32 locationInView:-Methode 54 Löschen-Geste 78–80
M mainScreen-Methode 198 MainWindow-iPad-Nib 27 makePopSound:-Methode 76–77 Model-Objekt ergänzen 8, 10 Kommunikation mit RootViewController 10–11 Monty Hall-Problem 210–211 movieDurationAvailable:-Methode 145, 171, 173 movieForView-Objekt 138
Movie-Player 156 als Feature 243 angepasste Wiedergabesteuerung 163–169 Anzeigeinformationen hinzufügen 139–142 einen View einrichten für 133–134, 139 Notifikationen 143–145 Positionssucher 173, 177 Spielzeit 170, 173 Steuerelemente implementieren 169–170 Thumbnails erstellen 150, 153, 156–157 Überblick 133 Video-Shoutouts 159–160, 163 Vollbildmodus 182–183 Wiedergabeliste hinzufügen 145– 150 Wiedergabesteuerung 178, 181 MoviePlayerViewController 135 movieURL-Methode 190 MPMovieLoadStateUnknownMethode 192 MPMoviePlayerController 134, 139 MPMovieTimeOptionExact 151 MPMovieTimeOptionNearestKey Frame 151 Multi-Touch-Events 55–58
N naturalSize-Eigenschaft 141 navigationsbasierte Anwendungsvorlage 8 Navigationscontroller 28 Navigationsview Ausrichtung 27 Nibs File's Owner-Objekt 135 iPad vs. iPhone 18 nicht-atomar 29 Notifikationen 109–110, 143–145, 172, 196 NSTimeInterval 150 NSTimer-Methode 161 NSURL-Objekt 137
RootViewController 28 W 253 numberOfTouchesRequired 61 Nutzer berücksichtigen 240
O onScreenDisplayLabel 140
P pathToWarningFile 130 path-Variable 118, 121, 123 PDF, speichern als 129–131 Peers bekannt machen 221 chatten 224 Notifikationen von 212 Picker-Controller für 222 verbinden 212, 222–223 Verbindung trennen 220 placePrizeBehindDoor:-Methode 217 playbackSliderDone:-Methode 175 playbackSliderMoved:-Methode 175, 181 playbackTime 150 playerDidEnterFullscreen:-Methode 182 player-Eigenschaft 137 playerPlaybackDidFinish:-Selektor 149 playerThumbnailImageRequest DidFinish:-Methode 154 PlaylistController 145 Popover 100 anzeigen 89–92 Ausrichtung und 97–99 den Controller glätten 88–89 den modalen View einblenden 85– 88 entfernen 45 Farb-Controller für 84–85 hinzufügen 44 Split-View und 92–93 Touch und 82–84 von Buttons 93–96 popoverController 43, 91 popoverControllerDidDismiss Popover:-Methode 96
Portrait-Ausrichtung Bildschirmgröße 20 entwerfen für 240 Start der Anwendung 21 Positionssucher für Videos 173, 177 preparePopSound:-Methode 76–77 presentOpenInMenuFromRect: inView:animated:-Methode 237 presentPreviewAnimated:-Methode 237 Programming with Quartz, 2D, and PDF Graphics in Mac OS X (Gelphman) 116 progressive Videoausstrahlung 186 Projekteinstellungen 14
Q Quartz 2D Graphics for Mac OS X Developers (Thompson) 116
R readonly-Eigenschaft 142 receiveData:fromPeer:inSession: context:-Methode 218 Rechtecke zeichnen 123, 129 Recognizer 52 an Thumbnail anknüpfen 156 automatisch freigeben 83 diskrete Gesten 65 konkurrierende 78–80 kontinuierliche Gesten 66 removeFromSuperview-Methode 59 removeView 163 requestThumbnailBilderAtTimes:time Option:-Methode 154 requireGestureRecognizerToFail:Methode 79 reset-Methode 70 Ressourcen freigeben 29, 37 restoreContext:-Methode 119 retain 29 revealDoor:-Methode 216 Roettgers, Janko 3 rootVC 32 RootViewController 28
254 X scalingMode-Eigenschaft 141
S scalingMode-Eigenschaft 141 screenDidConnectNotification:Methode 199, 202 screenDidDisconnectNotification:Methode 197 screens-Methode 198 Segmentierung von Videos 187 selectedRow-Variable 150 self.player.duration 145 sendDataToAllPeers:-Methode 224 sendMessage:forDoor:-Methode 217–218 Server, für Teilnehmeranwendung starten 213 session:didRecieveConnection RequestFromPeer:-Methode 212 session:peer:didChangeState:Methode 212 setbackgroundColor:-Methode 200, 202 setControlsTimer:-Methode 179 setDataReceiveHandler:-Methode 213 setFullscreen:animated-Methode 182 setNumberOf TapsRequired:-Methode 53 setupExternalScreen:-Methode 201 shouldAutorotateToInterface Orientation 21, 31 Shoutouts, Video 159–160, 163 showDriveControls-Methode 94, 96 showOrHideDriveControls:-Methode 96 Simulator, Kompatibilitätsmodus und 14 sliding-Variable 177 Speicher aufräumen 29 Speichern von Dokumenten 231 spezifische Unterklassen für Geräte 36 Splash-Screens 18 erstellen 20 Größe von 20 splitVC 29
Split-View entwerfen für 241 Landscape-Modus 22 Popover und 92–93 Split-View-basierte Vorlage 8 Split-View-Controller 25–47 als reine iPad-App 45–46 App-Delegates, trennen 36–39 Delegate für 40–42 Hintergrund für 27–29 Kommunikation zwischen Controllern 33 Outlet für 29 Popover entfernen 45 Popover hinzufügen 42–44 spezifische Unterklassen für die Geräte 33–36 Toolbar, dem Detail-View hinzufügen 40 verbinden 29 Spotlight, Symbole für 20 Status-Label einrichten 196 Streaming 185 Anwendungen mit 244 HTTP-Protokoll 186, 188–189 Netzwerkumgebung 193 Player erstellen für 190–191 progressives Video vs. VideoStreaming 186 Überblick 185, 193 Strich 119, 122 switchMovie-Methode 205
T Table-View 13–14 tableView:didSelectRowAtIndexPath: 32, 34–35 tableView-Methode 9, 14 Tap-Gesten 52–55, 156 Tastaturen 101–114 accessory-View 107–108 einfache Texteingabe 102–103 erstellen 104–105 Notifikationen 109–110 selbstdefinierte Buttons 105–107
Wischgeste 58–61, 78–80 W 255 Texteingabe 103 textViewShouldEndEditing:-Methode 230 The Daily Shoot 6 Thumbnails erstellen 150, 153, 156–157 timeOption-Parameter 150 Toolbar zu Detail-View hinzufügen 39–40 toolbar-Eigenschaft 39 touchesBegan:withEvent:-Methode 70–71 touchesCancelled:withEvent:Methode 70, 72 touchesEnded:withEvent:-Methode 70, 72 touchesMoved:withEvent:-Methode 70–71 triangle-Methode 118
U UIBezierPath-Klasse 120, 127 UIButton-View 139 UIDocumentInteractionController Dateien öffnen mit 234–237 Dateivorschau mit 237 UIGestureRecognizer 68 UIGraphicsBeginPDFContextToDataMethode 130 UIGraphicsBeginPDFContextToFileMethode 130 UIGraphicsBeginPDFPage-Methode 131 UIGraphicsBeginPDFPageWithInfoMethode 131 UIPopoverController 25 UIScreen-Klasse 196 UISplitViewController 25 UISplitViewControllerDelegate 42 UISwipeGestureRecognizer 58–61 universelle Anwendungen 16–18 updatePlaybackTime:-Methode 171, 177
updateTextViewWithMood:-Methode 107 Upgrade current target for iPadOption 16 userInfo-Parameter 163 Utility Application-Vorlage 8
V Verpixelung 15 verzögerte Initialisierung 222 video-Eigenschaft 138 Video-Positionssucher 173, 177 Video-Shoutouts 159–163 View-Controller 11–13 Kommunikation zwischen Controllern 32 viewDidLoad:-Methode 31, 46, 62, 77, 105, 109, 146, 173, 195, 199 viewForMovie 205 View-Hierarchie Multi-Touch-Events und 55–58 viewToDelete 75 virtuelle Luftpolsterfolie 50–51 eigene Gesten 65, 75 Folie löschen 59 Knallgeräusch 76–77 konkurrierende Gesten 78–80 kontinuierliche Gesten 61, 65–66 Multi-Touch-Events 58 Tap-Gesten 52–55 UISwipeGestureRecognizer 58–61 Vollbildmodus, Movie-Player 182–183 Vorlagen, Updates in 8
W Wiedergabeliste, zu Movie-Player hinzufügen 145–150 Wiedergabesteuerung dynamisch machen 178–181 eigene 163, 169 Window-basierte Vorlage 8 Wischgeste 58–61, 78–80
256 X Zeichnen 115–132
Z Zeichnen 115–132 Cocoa API 120–123 Core Graphics 116–120 Datei speichern 129–131 den Pfad verschieben 118–121 Dreieckspfad erstellen 117
Entwurfsplan für 117 Kreise und Rechtecke 123, 129 Strategie für 116 Strich und Füllung 119, 122 Zeichnungen als PDF speichern 129–131 Zusammenarbeit 243